From e82b72275b5b4f74a27fcffc943a35499d123079 Mon Sep 17 00:00:00 2001 From: dswbx Date: Tue, 4 Mar 2025 11:21:33 +0100 Subject: [PATCH 01/42] moved flash message, removed theme from admin controller --- app/src/modules/server/AdminController.tsx | 125 ++++++++++----------- app/src/ui/Admin.tsx | 14 +-- app/src/ui/modules/server/FlashMessage.tsx | 2 +- app/src/ui/routes/index.tsx | 4 +- 4 files changed, 71 insertions(+), 74 deletions(-) diff --git a/app/src/modules/server/AdminController.tsx b/app/src/modules/server/AdminController.tsx index 7d48bb0..b868807 100644 --- a/app/src/modules/server/AdminController.tsx +++ b/app/src/modules/server/AdminController.tsx @@ -5,6 +5,7 @@ import { config, isDebug } from "core"; import { addFlashMessage } from "core/server/flash"; import { html } from "hono/html"; import { Fragment } from "hono/jsx"; +import { css, Style } from "hono/css"; import { Controller } from "modules/Controller"; import * as SystemPermissions from "modules/permissions"; import type { AppTheme } from "modules/server/AppServer"; @@ -23,7 +24,7 @@ export type AdminControllerOptions = { export class AdminController extends Controller { constructor( private readonly app: App, - private _options: AdminControllerOptions = {} + private _options: AdminControllerOptions = {}, ) { super(); } @@ -36,7 +37,7 @@ export class AdminController extends Controller { return { ...this._options, basepath: this._options.basepath ?? "/", - assets_path: this._options.assets_path ?? config.server.assets_path + assets_path: this._options.assets_path ?? config.server.assets_path, }; } @@ -53,7 +54,7 @@ export class AdminController extends Controller { const hono = this.create().use( authMiddleware({ //skip: [/favicon\.ico$/] - }) + }), ); const auth = this.app.module.auth; @@ -66,14 +67,14 @@ export class AdminController extends Controller { success: configs.auth.cookie.pathSuccess ?? "/", loggedOut: configs.auth.cookie.pathLoggedOut ?? "/", login: "/auth/login", - logout: "/auth/logout" + logout: "/auth/logout", }; hono.use("*", async (c, next) => { const obj = { user: c.get("auth")?.user, logout_route: this.withBasePath(authRoutes.logout), - color_scheme: configs.server.admin.color_scheme + color_scheme: configs.server.admin.color_scheme, }; const html = await this.getHtml(obj); if (!html) { @@ -97,11 +98,11 @@ export class AdminController extends Controller { console.log("redirecting to success"); return c.redirect(authRoutes.success); } - } + }, }), async (c) => { return c.html(c.get("html")!); - } + }, ); hono.get(authRoutes.logout, async (c) => { @@ -119,16 +120,16 @@ export class AdminController extends Controller { console.log("redirecting"); return c.redirect(authRoutes.login); - } + }, }), permission(SystemPermissions.schemaRead, { onDenied: async (c) => { addFlashMessage(c, "You not allowed to read the schema", "warning"); - } + }, }), async (c) => { return c.html(c.get("html")!); - } + }, ); return hono; @@ -141,12 +142,12 @@ export class AdminController extends Controller { if (this.options.html.includes(htmlBkndContextReplace)) { return this.options.html.replace( htmlBkndContextReplace, - "" + "", ); } console.warn( - `Custom HTML needs to include '${htmlBkndContextReplace}' to inject BKND context` + `Custom HTML needs to include '${htmlBkndContextReplace}' to inject BKND context`, ); return this.options.html as string; } @@ -160,27 +161,36 @@ export class AdminController extends Controller { const assets = { js: "main.js", - css: "styles.css" + css: "styles.css", }; if (isProd) { - // @ts-ignore - const manifest = await import("bknd/dist/manifest.json", { - assert: { type: "json" } - }); + let manifest: any; + if (this.options.assets_path.startsWith("http")) { + manifest = await fetch(this.options.assets_path + "manifest.json", { + headers: { + Accept: "application/json", + }, + }).then((res) => res.json()); + } else { + // @ts-ignore + manifest = await import("bknd/dist/manifest.json", { + assert: { type: "json" }, + }).then((res) => res.default); + } + // @todo: load all marked as entry (incl. css) - assets.js = manifest.default["src/ui/main.tsx"].file; - assets.css = manifest.default["src/ui/main.tsx"].css[0] as any; + assets.js = manifest["src/ui/main.tsx"].file; + assets.css = manifest["src/ui/main.tsx"].css[0] as any; } - const theme = configs.server.admin.color_scheme ?? "light"; const favicon = isProd ? this.options.assets_path + "favicon.ico" : "/favicon.ico"; return ( {/* dnd complains otherwise */} {html``} - + - + + diff --git a/examples/react/package.json b/examples/react/package.json new file mode 100644 index 0000000..4a0bfca --- /dev/null +++ b/examples/react/package.json @@ -0,0 +1,28 @@ +{ + "name": "react", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "bknd": "file:../../app", + "@bknd/sqlocal": "file:../../packages/sqlocal", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "sqlocal": "^0.14.0" + }, + "devDependencies": { + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "globals": "^15.15.0", + "typescript": "~5.7.2", + "typescript-eslint": "^8.24.1", + "vite": "^6.2.0" + } +} diff --git a/examples/react/src/App.tsx b/examples/react/src/App.tsx new file mode 100644 index 0000000..1c9d48a --- /dev/null +++ b/examples/react/src/App.tsx @@ -0,0 +1,71 @@ +import { useEffect, useState } from "react"; +import { App } from "bknd"; +import { Admin } from "bknd/ui"; +import { checksum } from "bknd/utils"; +import { em, entity, text } from "bknd/data"; +import { SQLocalConnection } from "@bknd/sqlocal"; +import "bknd/dist/styles.css"; + +export default function () { + const [app, setApp] = useState(undefined); + const [hash, setHash] = useState(""); + + async function onBuilt(app: App) { + setApp(app); + setHash(await checksum(app.toJSON())); + } + + useEffect(() => { + setup({ + onBuilt, + }) + .then((app) => console.log("setup", app?.version())) + .catch(console.error); + }, []); + + if (!app) return null; + + return ( + // @ts-ignore + + ); +} + +let initialized = false; +export async function setup(opts?: { + beforeBuild?: (app: App) => Promise; + onBuilt?: (app: App) => Promise; +}) { + if (initialized) return; + initialized = true; + + const connection = new SQLocalConnection({ + verbose: true, + }); + + const app = App.create({ + connection, + initialConfig: { + data: em({ + test: entity("test", { + name: text(), + }), + }).toJSON(), + }, + }); + + if (opts?.onBuilt) { + app.emgr.onEvent( + App.Events.AppBuiltEvent, + async () => { + await opts.onBuilt?.(app); + }, + "sync", + ); + } + + await opts?.beforeBuild?.(app); + await app.build({ sync: true }); + + return app; +} diff --git a/examples/react/src/main.tsx b/examples/react/src/main.tsx new file mode 100644 index 0000000..d0da966 --- /dev/null +++ b/examples/react/src/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import App from "./App.tsx"; + +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/examples/react/src/vite-env.d.ts b/examples/react/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/examples/react/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/react/tsconfig.json b/examples/react/tsconfig.json new file mode 100644 index 0000000..e9c7ab7 --- /dev/null +++ b/examples/react/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + "strict": true, + "noImplicitAny": false, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/examples/react/vite.config.ts b/examples/react/vite.config.ts new file mode 100644 index 0000000..542ddcd --- /dev/null +++ b/examples/react/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +// https://vite.dev/config/ +// https://sqlocal.dallashoffman.com/guide/setup#vite-configuration +export default defineConfig({ + optimizeDeps: { + exclude: ["sqlocal"], + }, + plugins: [ + react(), + { + name: "configure-response-headers", + configureServer: (server) => { + server.middlewares.use((_req, res, next) => { + res.setHeader("Cross-Origin-Embedder-Policy", "require-corp"); + res.setHeader("Cross-Origin-Opener-Policy", "same-origin"); + next(); + }); + }, + }, + ], +}); diff --git a/packages/sqlocal/README.md b/packages/sqlocal/README.md new file mode 100644 index 0000000..5f4a6ff --- /dev/null +++ b/packages/sqlocal/README.md @@ -0,0 +1,29 @@ +# SQLocal adapter for `bknd` (experimental) +This packages adds an adapter to use a SQLocal database with `bknd`. It is based on [`sqlocal`](https://github.com/DallasHoff/sqlocal) and the driver included for `kysely`. + +## Installation +Install the adapter with: +```bash +npm install @bknd/sqlocal +``` + +## Usage +Create a connection: + +```ts +import { SQLocalConnection } from "@bknd/sqlocal"; + +const connection = new SQLocalConnection({ + databasePath: "db.sqlite" +}); +``` + +Use the connection depending on which framework or runtime you are using. E.g., when using `createApp`, you can use the connection as follows: + +```ts +import { createApp } from "bknd"; +import { SQLocalConnection } from "@bknd/sqlocal"; + +const connection = new SQLocalConnection(); +const app = createApp({ connection }); +``` \ No newline at end of file diff --git a/packages/sqlocal/package.json b/packages/sqlocal/package.json new file mode 100644 index 0000000..c2c5fcc --- /dev/null +++ b/packages/sqlocal/package.json @@ -0,0 +1,39 @@ +{ + "name": "@bknd/sqlocal", + "version": "0.0.1", + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsup", + "test": "vitest", + "typecheck": "tsc --noEmit", + "prepublishOnly": "bun run test && bun run typecheck && bun run build" + }, + "dependencies": { + "sqlocal": "^0.14.0" + }, + "devDependencies": { + "@vitest/browser": "^3.0.8", + "@vitest/ui": "^3.0.8", + "@types/node": "^22.13.10", + "bknd": "workspace:*", + "kysely": "^0.27.6", + "tsup": "^8.4.0", + "typescript": "^5.6.3", + "vitest": "^3.0.8", + "webdriverio": "^9.12.0" + }, + "tsup": { + "entry": ["src/index.ts"], + "format": ["esm"], + "target": "es2022", + "clean": true, + "minify": true, + "dts": true, + "metafile": true, + "external": ["bknd", "kysely"] + }, + "files": ["dist", "README.md", "!*.map", "!metafile*.json"] +} diff --git a/packages/sqlocal/src/SQLocalConnection.ts b/packages/sqlocal/src/SQLocalConnection.ts new file mode 100644 index 0000000..8b6bb98 --- /dev/null +++ b/packages/sqlocal/src/SQLocalConnection.ts @@ -0,0 +1,51 @@ +import { Kysely, ParseJSONResultsPlugin } from "kysely"; +import { SqliteConnection, SqliteIntrospector } from "bknd/data"; +import { SQLocalKysely } from "sqlocal/kysely"; +import type { ClientConfig } from "sqlocal"; + +const plugins = [new ParseJSONResultsPlugin()]; + +export type SQLocalConnectionConfig = Omit & { + // make it optional + databasePath?: ClientConfig["databasePath"]; +}; + +export class SQLocalConnection extends SqliteConnection { + private _client: SQLocalKysely | undefined; + + constructor(private config: SQLocalConnectionConfig) { + super(null as any, {}, plugins); + } + + override async init() { + if (this.initialized) return; + + await new Promise((resolve) => { + this._client = new SQLocalKysely({ + ...this.config, + databasePath: this.config.databasePath ?? "session", + onConnect: (r) => { + this.kysely = new Kysely({ + dialect: { + ...this._client!.dialect, + createIntrospector: (db: Kysely) => { + return new SqliteIntrospector(db, { + plugins, + }); + }, + }, + plugins, + }); + this.config.onConnect?.(r); + resolve(1); + }, + }); + }); + super.init(); + } + + get client(): SQLocalKysely { + if (!this._client) throw new Error("Client not initialized"); + return this._client!; + } +} diff --git a/packages/sqlocal/src/index.ts b/packages/sqlocal/src/index.ts new file mode 100644 index 0000000..44642fe --- /dev/null +++ b/packages/sqlocal/src/index.ts @@ -0,0 +1 @@ +export { SQLocalConnection, type SQLocalConnectionConfig } from "./SQLocalConnection"; diff --git a/packages/sqlocal/test/base.test.ts b/packages/sqlocal/test/base.test.ts new file mode 100644 index 0000000..661c61a --- /dev/null +++ b/packages/sqlocal/test/base.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from "vitest"; +import { SQLocal } from "sqlocal"; + +describe("base", () => { + const { sql } = new SQLocal(":memory:"); + + it("works", async () => { + expect(await sql`SELECT 1`).toEqual([{ "1": 1 }]); + }); +}); diff --git a/packages/sqlocal/test/connection.test.ts b/packages/sqlocal/test/connection.test.ts new file mode 100644 index 0000000..f43ccc9 --- /dev/null +++ b/packages/sqlocal/test/connection.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest"; +import { SQLocalConnection, type SQLocalConnectionConfig } from "../src"; + +describe(SQLocalConnection, () => { + function create(config: SQLocalConnectionConfig = {}) { + return new SQLocalConnection(config); + } + + it("constructs", async () => { + const connection = create(); + expect(() => connection.client).toThrow(); + await connection.init(); + expect(connection.client).toBeDefined(); + expect(await connection.client.sql`SELECT 1`).toEqual([{ "1": 1 }]); + }); +}); diff --git a/packages/sqlocal/test/integration.test.ts b/packages/sqlocal/test/integration.test.ts new file mode 100644 index 0000000..16d4ccf --- /dev/null +++ b/packages/sqlocal/test/integration.test.ts @@ -0,0 +1,100 @@ +import { describe, expect, it } from "vitest"; +import { SQLocalConnection, type SQLocalConnectionConfig } from "../src"; +import { createApp } from "bknd"; +import * as proto from "bknd/data"; + +describe("integration", () => { + function create(config: SQLocalConnectionConfig = { databasePath: ":memory:" }) { + return new SQLocalConnection(config); + } + + it("should create app and ping", async () => { + const app = createApp({ + connection: create(), + }); + await app.build(); + + expect(app.version()).toBeDefined(); + expect(await app.em.ping()).toBe(true); + }); + + it("should create a basic schema", async () => { + const schema = proto.em( + { + posts: proto.entity("posts", { + title: proto.text().required(), + content: proto.text(), + }), + comments: proto.entity("comments", { + content: proto.text(), + }), + }, + (fns, s) => { + fns.relation(s.comments).manyToOne(s.posts); + fns.index(s.posts).on(["title"], true); + }, + ); + + const app = createApp({ + connection: create(), + initialConfig: { + data: schema.toJSON(), + }, + }); + + await app.build(); + + expect(app.em.entities.length).toBe(2); + expect(app.em.entities.map((e) => e.name)).toEqual(["posts", "comments"]); + + const api = app.getApi(); + + expect( + ( + await api.data.createMany("posts", [ + { + title: "Hello", + content: "World", + }, + { + title: "Hello 2", + content: "World 2", + }, + ]) + ).data, + ).toEqual([ + { + id: 1, + title: "Hello", + content: "World", + }, + { + id: 2, + title: "Hello 2", + content: "World 2", + }, + ] as any); + + // try to create an existing + expect( + ( + await api.data.createOne("posts", { + title: "Hello", + }) + ).ok, + ).toBe(false); + + // add a comment to a post + await api.data.createOne("comments", { + content: "Hello", + posts_id: 1, + }); + + // and then query using a `with` property + const result = await api.data.readMany("posts", { with: ["comments"] }); + expect(result.length).toBe(2); + expect(result[0].comments.length).toBe(1); + expect(result[0].comments[0].content).toBe("Hello"); + expect(result[1].comments.length).toBe(0); + }); +}); diff --git a/packages/sqlocal/tsconfig.json b/packages/sqlocal/tsconfig.json new file mode 100644 index 0000000..d2359e0 --- /dev/null +++ b/packages/sqlocal/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "composite": false, + "module": "ESNext", + "moduleResolution": "bundler", + "allowImportingTsExtensions": false, + "target": "ES2022", + "noImplicitAny": false, + "allowJs": true, + "verbatimModuleSyntax": true, + "declaration": true, + "strict": true, + "allowUnusedLabels": false, + "allowUnreachableCode": false, + "exactOptionalPropertyTypes": false, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noPropertyAccessFromIndexSignature": false, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "isolatedModules": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["./src/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/packages/sqlocal/vitest.config.ts b/packages/sqlocal/vitest.config.ts new file mode 100644 index 0000000..f55215a --- /dev/null +++ b/packages/sqlocal/vitest.config.ts @@ -0,0 +1,36 @@ +/// +/// +import { defineConfig } from "vite"; + +// https://github.com/DallasHoff/sqlocal/blob/main/vite.config.ts +export default defineConfig({ + test: { + testTimeout: 1000, + hookTimeout: 1000, + teardownTimeout: 1000, + includeTaskLocation: true, + browser: { + enabled: true, + headless: true, + screenshotFailures: false, + provider: "webdriverio", + instances: [{ browser: "chrome" }], + }, + }, + optimizeDeps: { + exclude: ["@sqlite.org/sqlite-wasm"], + }, + plugins: [ + { + enforce: "pre", + name: "configure-response-headers", + configureServer: (server) => { + server.middlewares.use((_req, res, next) => { + res.setHeader("Cross-Origin-Embedder-Policy", "require-corp"); + res.setHeader("Cross-Origin-Opener-Policy", "same-origin"); + next(); + }); + }, + }, + ], +}); From b994c2dfb0ace9eb2211a4f97a8014cb2b527188 Mon Sep 17 00:00:00 2001 From: dswbx Date: Sat, 15 Mar 2025 15:07:29 +0100 Subject: [PATCH 33/42] bump rc version --- app/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/package.json b/app/package.json index 463e9c9..364daf5 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "type": "module", "sideEffects": false, "bin": "./dist/cli/index.js", - "version": "0.10.0-rc.3", + "version": "0.10.0-rc.5", "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": { From 0e81e14421e5006e4c1000d12b8f8a7999e5cdd1 Mon Sep 17 00:00:00 2001 From: dswbx Date: Sat, 15 Mar 2025 15:08:04 +0100 Subject: [PATCH 34/42] bump rc version --- bun.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bun.lock b/bun.lock index cdf4852..3218ebf 100644 --- a/bun.lock +++ b/bun.lock @@ -27,7 +27,7 @@ }, "app": { "name": "bknd", - "version": "0.10.0-rc.4", + "version": "0.10.0-rc.5", "bin": "./dist/cli/index.js", "dependencies": { "@cfworker/json-schema": "^4.1.1", From 2531c2d8d49ef9c4bf67c1511554ad6f9fa7fa74 Mon Sep 17 00:00:00 2001 From: dswbx Date: Sat, 15 Mar 2025 16:37:03 +0100 Subject: [PATCH 35/42] updated react example with todo example --- bun.lock | 6 +- examples/react/package.json | 10 ++- examples/react/public/bknd.svg | 14 ++++ examples/react/src/App.tsx | 90 ++++++++++++++++++----- examples/react/src/components/Center.tsx | 10 +++ examples/react/src/main.tsx | 3 +- examples/react/src/routes/_index.tsx | 94 ++++++++++++++++++++++++ examples/react/src/routes/admin.tsx | 9 +++ examples/react/src/styles.css | 25 +++++++ examples/react/tsconfig.json | 7 +- examples/react/vite.config.ts | 4 + packages/postgres/README.md | 2 +- packages/postgres/package.json | 11 ++- packages/sqlocal/README.md | 2 +- packages/sqlocal/package.json | 11 ++- 15 files changed, 261 insertions(+), 37 deletions(-) create mode 100644 examples/react/public/bknd.svg create mode 100644 examples/react/src/components/Center.tsx create mode 100644 examples/react/src/routes/_index.tsx create mode 100644 examples/react/src/routes/admin.tsx create mode 100644 examples/react/src/styles.css diff --git a/bun.lock b/bun.lock index 3218ebf..76abf42 100644 --- a/bun.lock +++ b/bun.lock @@ -143,7 +143,7 @@ "version": "0.0.1", "dependencies": { "kysely": "^0.27.6", - "pg": "^8.12.0", + "pg": "^8.14.0", }, "devDependencies": { "@types/bun": "^1.2.5", @@ -151,7 +151,7 @@ "@types/pg": "^8.11.11", "bknd": "workspace:*", "tsup": "^8.4.0", - "typescript": "^5.6.3", + "typescript": "^5.8.2", }, }, "packages/sqlocal": { @@ -167,7 +167,7 @@ "bknd": "workspace:*", "kysely": "^0.27.6", "tsup": "^8.4.0", - "typescript": "^5.6.3", + "typescript": "^5.8.2", "vitest": "^3.0.8", "webdriverio": "^9.12.0", }, diff --git a/examples/react/package.json b/examples/react/package.json index 4a0bfca..568b72d 100644 --- a/examples/react/package.json +++ b/examples/react/package.json @@ -10,19 +10,23 @@ "preview": "vite preview" }, "dependencies": { - "bknd": "file:../../app", "@bknd/sqlocal": "file:../../packages/sqlocal", + "bknd": "file:../../app", "react": "^19.0.0", "react-dom": "^19.0.0", - "sqlocal": "^0.14.0" + "sqlocal": "^0.14.0", + "wouter": "^3.6.0" }, "devDependencies": { + "@tailwindcss/vite": "^4.0.14", "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", "@vitejs/plugin-react": "^4.3.4", "globals": "^15.15.0", + "tailwindcss": "^4.0.14", "typescript": "~5.7.2", "typescript-eslint": "^8.24.1", - "vite": "^6.2.0" + "vite": "^6.2.0", + "vite-tsconfig-paths": "^5.1.4" } } diff --git a/examples/react/public/bknd.svg b/examples/react/public/bknd.svg new file mode 100644 index 0000000..182ef92 --- /dev/null +++ b/examples/react/public/bknd.svg @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/examples/react/src/App.tsx b/examples/react/src/App.tsx index 1c9d48a..e122556 100644 --- a/examples/react/src/App.tsx +++ b/examples/react/src/App.tsx @@ -1,9 +1,13 @@ -import { useEffect, useState } from "react"; +import { createContext, lazy, useEffect, useState, Suspense, Fragment } from "react"; import { App } from "bknd"; -import { Admin } from "bknd/ui"; -import { checksum } from "bknd/utils"; -import { em, entity, text } from "bknd/data"; +import { checksum, secureRandomString } from "bknd/utils"; +import { boolean, em, entity, text } from "bknd/data"; import { SQLocalConnection } from "@bknd/sqlocal"; +import { Route, Router, Switch } from "wouter"; +import IndexPage from "~/routes/_index"; +const Admin = lazy(() => import("~/routes/admin")); +import { Center } from "~/components/Center"; +import { ClientProvider } from "bknd/client"; import "bknd/dist/styles.css"; export default function () { @@ -11,28 +15,65 @@ export default function () { const [hash, setHash] = useState(""); async function onBuilt(app: App) { - setApp(app); - setHash(await checksum(app.toJSON())); + document.startViewTransition(async () => { + setApp(app); + setHash(await checksum(app.toJSON())); + }); } useEffect(() => { - setup({ - onBuilt, - }) + setup({ onBuilt }) .then((app) => console.log("setup", app?.version())) .catch(console.error); }, []); - if (!app) return null; + if (!app) + return ( +
+ Loading... +
+ ); return ( - // @ts-ignore - + + + ( + + + + )} + /> + + + + + + + +
404
+
+
+
); } +const schema = em({ + todos: entity("todos", { + title: text(), + done: boolean(), + }), +}); + +// register your schema to get automatic type completion +type Database = (typeof schema)["DB"]; +declare module "bknd/core" { + interface DB extends Database {} +} + let initialized = false; -export async function setup(opts?: { +async function setup(opts?: { beforeBuild?: (app: App) => Promise; onBuilt?: (app: App) => Promise; }) { @@ -40,17 +81,30 @@ export async function setup(opts?: { initialized = true; const connection = new SQLocalConnection({ + databasePath: ":localStorage:", verbose: true, }); const app = App.create({ connection, + // an initial config is only applied if the database is empty initialConfig: { - data: em({ - test: entity("test", { - name: text(), - }), - }).toJSON(), + data: schema.toJSON(), + }, + 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", + });*/ + }, }, }); diff --git a/examples/react/src/components/Center.tsx b/examples/react/src/components/Center.tsx new file mode 100644 index 0000000..235d8c5 --- /dev/null +++ b/examples/react/src/components/Center.tsx @@ -0,0 +1,10 @@ +import type { ComponentProps } from "react"; + +export function Center(props: ComponentProps<"div">) { + return ( +
+ ); +} diff --git a/examples/react/src/main.tsx b/examples/react/src/main.tsx index d0da966..1215d5e 100644 --- a/examples/react/src/main.tsx +++ b/examples/react/src/main.tsx @@ -1,6 +1,7 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; -import App from "./App.tsx"; +import App from "./App"; +import "./styles.css"; createRoot(document.getElementById("root")!).render( diff --git a/examples/react/src/routes/_index.tsx b/examples/react/src/routes/_index.tsx new file mode 100644 index 0000000..227ab5f --- /dev/null +++ b/examples/react/src/routes/_index.tsx @@ -0,0 +1,94 @@ +import { Center } from "~/components/Center"; +import type { App } from "bknd"; +import { useEntityQuery } from "bknd/client"; + +export default function IndexPage({ app }: { app: App }) { + const user = app.getApi().getUser(); + const limit = 5; + const { data: todos, ...$q } = useEntityQuery("todos", undefined, { + limit, + }); + // @ts-ignore + const total = todos?.body.meta.total || 0; + + return ( +
+
+
+ bknd +

local

+
+ +
+

+ What's next? ({total}) +

+
+ {total > limit && ( +
+ {total - limit} more todo(s) hidden +
+ )} +
+ {todos?.reverse().map((todo) => ( +
+
+ { + await $q.update({ done: !todo.done }, todo.id); + }} + /> +
{todo.title}
+
+ +
+ ))} +
+
t.id).join()} + action={async (formData: FormData) => { + const title = formData.get("title") as string; + await $q.create({ title }); + }} + > + + +
+
+
+ +
+ Go to Admin ➝ + {/*
+ {user ? ( +

+ Authenticated as {user.email} +

+ ) : ( + Login + )} +
*/} +
+
+
+ ); +} diff --git a/examples/react/src/routes/admin.tsx b/examples/react/src/routes/admin.tsx new file mode 100644 index 0000000..0f390af --- /dev/null +++ b/examples/react/src/routes/admin.tsx @@ -0,0 +1,9 @@ +import { Admin, type BkndAdminProps } from "bknd/ui"; +import type { App } from "bknd"; + +export default function AdminPage({ + app, + ...props +}: Omit & { app: App }) { + return ; +} diff --git a/examples/react/src/styles.css b/examples/react/src/styles.css new file mode 100644 index 0000000..3f4f365 --- /dev/null +++ b/examples/react/src/styles.css @@ -0,0 +1,25 @@ +/* @todo: currently not working nicely */ +#app { + @import "tailwindcss"; + :root { + --background: #ffffff; + --foreground: #171717; + } + + @media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } + } + + @theme { + --color-background: var(--background); + --color-foreground: var(--foreground); + } + + width: 100%; + min-height: 100dvh; + @apply bg-background text-foreground flex; + font-family: Arial, Helvetica, sans-serif; +} diff --git a/examples/react/tsconfig.json b/examples/react/tsconfig.json index e9c7ab7..9a3582b 100644 --- a/examples/react/tsconfig.json +++ b/examples/react/tsconfig.json @@ -7,16 +7,17 @@ "module": "ESNext", "skipLibCheck": true, "moduleResolution": "bundler", - "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", "noEmit": true, "jsx": "react-jsx", - "strict": true, "noImplicitAny": false, "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true + "noUncheckedSideEffectImports": true, + "paths": { + "~/*": ["./src/*"] + } }, "include": ["src"] } diff --git a/examples/react/vite.config.ts b/examples/react/vite.config.ts index 542ddcd..b374507 100644 --- a/examples/react/vite.config.ts +++ b/examples/react/vite.config.ts @@ -1,5 +1,7 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; +import tsconfigPaths from "vite-tsconfig-paths"; // https://vite.dev/config/ // https://sqlocal.dallashoffman.com/guide/setup#vite-configuration @@ -9,6 +11,8 @@ export default defineConfig({ }, plugins: [ react(), + tailwindcss(), + tsconfigPaths(), { name: "configure-response-headers", configureServer: (server) => { diff --git a/packages/postgres/README.md b/packages/postgres/README.md index cb22856..e04d8a1 100644 --- a/packages/postgres/README.md +++ b/packages/postgres/README.md @@ -1,5 +1,5 @@ # Postgres adapter for `bknd` (experimental) -This packages adds an adapter to use a Postgres database with `bknd`. It is based on `pg` and the driver included in `kysely`. +This packages adds an adapter to use a Postgres database with [`bknd`](https://github.com/bknd-io/bknd). It is based on [`pg`](https://github.com/brianc/node-postgres) and the driver included in [`kysely`](https://github.com/kysely-org/kysely). ## Installation Install the adapter with: diff --git a/packages/postgres/package.json b/packages/postgres/package.json index 51d0e90..681fffe 100644 --- a/packages/postgres/package.json +++ b/packages/postgres/package.json @@ -5,14 +5,20 @@ "main": "dist/index.js", "module": "dist/index.js", "types": "dist/index.d.ts", + "publishConfig": { + "access": "public" + }, "scripts": { "build": "tsup", "test": "bun test", + "typecheck": "tsc --noEmit", + "updater": "bun x npm-check-updates -ui", + "prepublishOnly": "bun run typecheck && bun run test && bun run build", "docker:start": "docker run --rm --name bknd-test-postgres -d -e POSTGRES_PASSWORD=postgres -e POSTGRES_USER=postgres -e POSTGRES_DB=bknd -p 5430:5432 postgres:17", "docker:stop": "docker stop bknd-test-postgres" }, "dependencies": { - "pg": "^8.12.0", + "pg": "^8.14.0", "kysely": "^0.27.6" }, "devDependencies": { @@ -21,7 +27,7 @@ "@types/pg": "^8.11.11", "bknd": "workspace:*", "tsup": "^8.4.0", - "typescript": "^5.6.3" + "typescript": "^5.8.2" }, "tsup": { "entry": ["src/index.ts"], @@ -30,7 +36,6 @@ "clean": true, "minify": true, "dts": true, - "metafile": true, "external": ["bknd", "pg", "kysely"] }, "files": ["dist", "README.md", "!*.map", "!metafile*.json"] diff --git a/packages/sqlocal/README.md b/packages/sqlocal/README.md index 5f4a6ff..4c09768 100644 --- a/packages/sqlocal/README.md +++ b/packages/sqlocal/README.md @@ -1,5 +1,5 @@ # SQLocal adapter for `bknd` (experimental) -This packages adds an adapter to use a SQLocal database with `bknd`. It is based on [`sqlocal`](https://github.com/DallasHoff/sqlocal) and the driver included for `kysely`. +This packages adds an adapter to use a SQLocal database with `bknd`](https://github.com/bknd-io/bknd). It is based on [`sqlocal`](https://github.com/DallasHoff/sqlocal) and the driver included for [`kysely`](https://github.com/kysely-org/kysely). ## Installation Install the adapter with: diff --git a/packages/sqlocal/package.json b/packages/sqlocal/package.json index c2c5fcc..1285c13 100644 --- a/packages/sqlocal/package.json +++ b/packages/sqlocal/package.json @@ -5,11 +5,15 @@ "main": "dist/index.js", "module": "dist/index.js", "types": "dist/index.d.ts", + "publishConfig": { + "access": "public" + }, "scripts": { "build": "tsup", - "test": "vitest", + "test": "vitest --run", + "updater": "bun x npm-check-updates -ui", "typecheck": "tsc --noEmit", - "prepublishOnly": "bun run test && bun run typecheck && bun run build" + "prepublishOnly": "bun run typecheck && bun run test && bun run build" }, "dependencies": { "sqlocal": "^0.14.0" @@ -21,7 +25,7 @@ "bknd": "workspace:*", "kysely": "^0.27.6", "tsup": "^8.4.0", - "typescript": "^5.6.3", + "typescript": "^5.8.2", "vitest": "^3.0.8", "webdriverio": "^9.12.0" }, @@ -32,7 +36,6 @@ "clean": true, "minify": true, "dts": true, - "metafile": true, "external": ["bknd", "kysely"] }, "files": ["dist", "README.md", "!*.map", "!metafile*.json"] From ea2aa7c76c4c5361e493a27584d526daa366f3d4 Mon Sep 17 00:00:00 2001 From: dswbx Date: Sat, 15 Mar 2025 16:44:58 +0100 Subject: [PATCH 36/42] fix react example styling --- examples/react/src/App.tsx | 1 - examples/react/src/routes/_index.tsx | 128 +++++++++++++-------------- examples/react/src/routes/admin.tsx | 1 + examples/react/src/styles.css | 39 ++++---- 4 files changed, 85 insertions(+), 84 deletions(-) diff --git a/examples/react/src/App.tsx b/examples/react/src/App.tsx index e122556..2e2a25b 100644 --- a/examples/react/src/App.tsx +++ b/examples/react/src/App.tsx @@ -8,7 +8,6 @@ import IndexPage from "~/routes/_index"; const Admin = lazy(() => import("~/routes/admin")); import { Center } from "~/components/Center"; import { ClientProvider } from "bknd/client"; -import "bknd/dist/styles.css"; export default function () { const [app, setApp] = useState(undefined); diff --git a/examples/react/src/routes/_index.tsx b/examples/react/src/routes/_index.tsx index 227ab5f..4f57317 100644 --- a/examples/react/src/routes/_index.tsx +++ b/examples/react/src/routes/_index.tsx @@ -12,73 +12,72 @@ export default function IndexPage({ app }: { app: App }) { const total = todos?.body.meta.total || 0; return ( -
-
-
- bknd -

local

-
+
+
+ bknd +

local

+
-
-

- What's next? ({total}) -

-
- {total > limit && ( -
- {total - limit} more todo(s) hidden -
- )} -
- {todos?.reverse().map((todo) => ( -
-
- { - await $q.update({ done: !todo.done }, todo.id); - }} - /> -
{todo.title}
-
- -
- ))} +
+

+ What's next? ({total}) +

+
+ {total > limit && ( +
+ {total - limit} more todo(s) hidden
-
t.id).join()} - action={async (formData: FormData) => { - const title = formData.get("title") as string; - await $q.create({ title }); - }} - > - - -
+ )} +
+ {todos?.reverse().map((todo) => ( +
+
+ { + await $q.update({ done: !todo.done }, todo.id); + }} + /> +
{todo.title}
+
+ +
+ ))}
+
t.id).join()} + action={async (formData: FormData) => { + const title = formData.get("title") as string; + await $q.create({ title }); + }} + > + + +
+
-
- Go to Admin ➝ - {/*
+
+ Go to Admin ➝ + {/*
{user ? (

Authenticated as {user.email} @@ -87,8 +86,7 @@ export default function IndexPage({ app }: { app: App }) { Login )}

*/} -
-
-
+
+ ); } diff --git a/examples/react/src/routes/admin.tsx b/examples/react/src/routes/admin.tsx index 0f390af..efb5d12 100644 --- a/examples/react/src/routes/admin.tsx +++ b/examples/react/src/routes/admin.tsx @@ -1,5 +1,6 @@ import { Admin, type BkndAdminProps } from "bknd/ui"; import type { App } from "bknd"; +import "bknd/dist/styles.css"; export default function AdminPage({ app, diff --git a/examples/react/src/styles.css b/examples/react/src/styles.css index 3f4f365..4e0bdd8 100644 --- a/examples/react/src/styles.css +++ b/examples/react/src/styles.css @@ -1,25 +1,28 @@ -/* @todo: currently not working nicely */ -#app { - @import "tailwindcss"; +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@media (prefers-color-scheme: dark) { :root { - --background: #ffffff; - --foreground: #171717; + --background: #0a0a0a; + --foreground: #ededed; } +} - @media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } - } +@theme { + --color-background: var(--background); + --color-foreground: var(--foreground); +} - @theme { - --color-background: var(--background); - --color-foreground: var(--foreground); - } - - width: 100%; - min-height: 100dvh; +body { @apply bg-background text-foreground flex; font-family: Arial, Helvetica, sans-serif; } + +#root { + width: 100%; + min-height: 100dvh; +} From f6996b1953599f54529a026b7f30d605e22edee3 Mon Sep 17 00:00:00 2001 From: dswbx Date: Tue, 18 Mar 2025 10:56:39 +0100 Subject: [PATCH 37/42] admin ui: started color centralization + made sidebar resizable --- app/src/ui/components/display/Alert.tsx | 16 +-- .../ui/components/form/Formy/components.tsx | 39 ++++++- .../form/json-schema-form/Field.tsx | 29 +++-- app/src/ui/layouts/AppShell/AppShell.tsx | 108 +++++++++++++----- app/src/ui/layouts/AppShell/Header.tsx | 10 +- app/src/ui/main.css | 49 ++++++-- .../components/canvas/DataSchemaCanvas.tsx | 2 - app/src/ui/routes/auth/auth.settings.tsx | 1 + app/src/ui/store/appshell.ts | 23 ++++ app/src/ui/store/index.ts | 1 + app/src/ui/store/utils.ts | 0 examples/react/src/App.tsx | 7 +- examples/react/src/routes/_index.tsx | 93 +++++++++++---- 13 files changed, 286 insertions(+), 92 deletions(-) create mode 100644 app/src/ui/store/appshell.ts create mode 100644 app/src/ui/store/index.ts create mode 100644 app/src/ui/store/utils.ts diff --git a/app/src/ui/components/display/Alert.tsx b/app/src/ui/components/display/Alert.tsx index de2d366..0cd4240 100644 --- a/app/src/ui/components/display/Alert.tsx +++ b/app/src/ui/components/display/Alert.tsx @@ -18,13 +18,7 @@ const Base: React.FC = ({ ...props }) => visible ? ( -
+

{title && {title}: } {message || children} @@ -33,19 +27,19 @@ const Base: React.FC = ({ ) : null; const Warning: React.FC = ({ className, ...props }) => ( - + ); const Exception: React.FC = ({ className, ...props }) => ( - + ); const Success: React.FC = ({ className, ...props }) => ( - + ); const Info: React.FC = ({ className, ...props }) => ( - + ); export const Alert = { diff --git a/app/src/ui/components/form/Formy/components.tsx b/app/src/ui/components/form/Formy/components.tsx index 9996139..4eb8cb4 100644 --- a/app/src/ui/components/form/Formy/components.tsx +++ b/app/src/ui/components/form/Formy/components.tsx @@ -3,7 +3,6 @@ import { getBrowser } from "core/utils"; import type { Field } from "data"; import { Switch as RadixSwitch } from "radix-ui"; import { - type ChangeEventHandler, type ComponentPropsWithoutRef, type ElementType, forwardRef, @@ -12,7 +11,7 @@ import { useRef, useState, } from "react"; -import { TbCalendar, TbChevronDown, TbInfoCircle } from "react-icons/tb"; +import { TbCalendar, TbChevronDown, TbEye, TbEyeOff, TbInfoCircle } from "react-icons/tb"; import { twMerge } from "tailwind-merge"; import { IconButton } from "ui/components/buttons/IconButton"; import { useEvent } from "ui/hooks/use-event"; @@ -89,7 +88,7 @@ export const Input = forwardRef> {...props} ref={ref} className={twMerge( - "bg-muted/40 h-11 rounded-md py-2.5 px-4 outline-none", + "bg-muted/40 h-11 rounded-md py-2.5 px-4 outline-none w-full", disabledOrReadonly && "bg-muted/50 text-primary/50", !disabledOrReadonly && "focus:bg-muted focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all", @@ -99,6 +98,40 @@ export const Input = forwardRef> ); }); +export const TypeAwareInput = forwardRef>( + (props, ref) => { + if (props.type === "password") { + return ; + } + + return ; + }, +); + +export const Password = forwardRef>( + (props, ref) => { + const [visible, setVisible] = useState(false); + + function handleToggle() { + setVisible((v) => !v); + } + + return ( +

+ +
+ +
+
+ ); + }, +); + export const Textarea = forwardRef>( (props, ref) => { return ( diff --git a/app/src/ui/components/form/json-schema-form/Field.tsx b/app/src/ui/components/form/json-schema-form/Field.tsx index 955d882..e511977 100644 --- a/app/src/ui/components/form/json-schema-form/Field.tsx +++ b/app/src/ui/components/form/json-schema-form/Field.tsx @@ -1,5 +1,5 @@ import type { JsonSchema } from "json-schema-library"; -import type { ChangeEvent, ComponentPropsWithoutRef } from "react"; +import type { ChangeEvent, ComponentPropsWithoutRef, ReactNode } from "react"; import ErrorBoundary from "ui/components/display/ErrorBoundary"; import * as Formy from "ui/components/form/Formy"; import { useEvent } from "ui/hooks/use-event"; @@ -13,6 +13,7 @@ export type FieldProps = { onChange?: (e: ChangeEvent) => void; placeholder?: string; disabled?: boolean; + inputProps?: Partial; } & Omit; export const Field = (props: FieldProps) => { @@ -31,7 +32,14 @@ const fieldErrorBoundary = ); -const FieldImpl = ({ name, onChange, placeholder, required: _required, ...props }: FieldProps) => { +const FieldImpl = ({ + name, + onChange, + placeholder, + required: _required, + inputProps, + ...props +}: FieldProps) => { const { path, setValue, schema, ...ctx } = useDerivedFieldContext(name); const required = typeof _required === "boolean" ? _required : ctx.required; //console.log("Field", { name, path, schema }); @@ -64,6 +72,7 @@ const FieldImpl = ({ name, onChange, placeholder, required: _required, ...props return ( ( ); -export const FieldComponent = ({ - schema, - ..._props -}: { schema: JsonSchema } & ComponentPropsWithoutRef<"input">) => { +export type FieldComponentProps = { + schema: JsonSchema; + render?: (props: Omit) => ReactNode; +} & ComponentPropsWithoutRef<"input">; + +export const FieldComponent = ({ schema, render, ..._props }: FieldComponentProps) => { const { value } = useFormValue(_props.name!, { strict: true }); if (!isTypeSchema(schema)) return null; const props = { @@ -97,6 +108,8 @@ export const FieldComponent = ({ : "", }; + if (render) return render({ schema, ...props }); + if (schema.enum) { return ; } @@ -158,5 +171,7 @@ export const FieldComponent = ({ } } - return ; + return ( + + ); }; diff --git a/app/src/ui/layouts/AppShell/AppShell.tsx b/app/src/ui/layouts/AppShell/AppShell.tsx index 25110d7..16cfc8b 100644 --- a/app/src/ui/layouts/AppShell/AppShell.tsx +++ b/app/src/ui/layouts/AppShell/AppShell.tsx @@ -1,5 +1,6 @@ import { useClickOutside, useHotkeys } from "@mantine/hooks"; import { IconChevronDown, IconChevronUp } from "@tabler/icons-react"; +import { clampNumber } from "core/utils/numbers"; import { throttle } from "lodash-es"; import { ScrollArea } from "radix-ui"; import { @@ -12,13 +13,20 @@ import { import type { IconType } from "react-icons"; import { twMerge } from "tailwind-merge"; import { IconButton } from "ui/components/buttons/IconButton"; -import { useEvent } from "ui/hooks/use-event"; import { AppShellProvider, useAppShell } from "ui/layouts/AppShell/use-appshell"; +import { appShellStore } from "ui/store"; +import { useLocation } from "wouter"; -export function Root({ children }) { +export function Root({ children }: { children: React.ReactNode }) { + const sidebarWidth = appShellStore((store) => store.sidebarWidth); return ( -
+
{children}
@@ -80,7 +88,7 @@ export function Main({ children }) { data-shell="main" className={twMerge( "flex flex-col flex-grow w-1 flex-shrink-1", - sidebar.open && "md:max-w-[calc(100%-350px)]", + sidebar.open && "md:max-w-[calc(100%-var(--sidebar-width))]", )} > {children} @@ -89,47 +97,38 @@ export function Main({ children }) { } export function Sidebar({ children }) { - const ctx = useAppShell(); + const open = appShellStore((store) => store.sidebarOpen); + const close = appShellStore((store) => store.closeSidebar); + const ref = useClickOutside(close, null, [document.getElementById("header")]); + const [location] = useLocation(); - const ref = useClickOutside(ctx.sidebar?.handler?.close); + const closeHandler = () => { + open && close(); + }; - const onClickBackdrop = useEvent((e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - ctx?.sidebar?.handler.close(); - }); - - const onEscape = useEvent(() => { - if (ctx?.sidebar?.open) { - ctx?.sidebar?.handler.close(); - } - }); + // listen for window location change + useEffect(closeHandler, [location]); // @todo: potentially has to be added to the root, as modals could be opened - useHotkeys([["Escape", onEscape]]); - - if (!ctx) { - console.warn("AppShell.Sidebar: missing AppShellContext"); - return null; - } + useHotkeys([["Escape", closeHandler]]); return ( <> +
@@ -138,6 +137,59 @@ export function Sidebar({ children }) { ); } +const SidebarResize = () => { + const setSidebarWidth = appShellStore((store) => store.setSidebarWidth); + const [isResizing, setIsResizing] = useState(false); + const [startX, setStartX] = useState(0); + const [startWidth, setStartWidth] = useState(0); + + const handleMouseDown = (e: React.MouseEvent) => { + e.preventDefault(); + setIsResizing(true); + setStartX(e.clientX); + setStartWidth( + Number.parseInt( + getComputedStyle(document.getElementById("app-shell")!) + .getPropertyValue("--sidebar-width") + .replace("px", ""), + ), + ); + }; + + const handleMouseMove = (e: MouseEvent) => { + if (!isResizing) return; + + const diff = e.clientX - startX; + const newWidth = clampNumber(startWidth + diff, 250, window.innerWidth * 0.5); + setSidebarWidth(newWidth); + }; + + const handleMouseUp = () => { + setIsResizing(false); + }; + + useEffect(() => { + if (isResizing) { + window.addEventListener("mousemove", handleMouseMove); + window.addEventListener("mouseup", handleMouseUp); + } + + return () => { + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mouseup", handleMouseUp); + }; + }, [isResizing, startX, startWidth]); + + return ( +
+ ); +}; + export function SectionHeaderTitle({ children, className, ...props }: ComponentProps<"h2">) { return (

- ); + const toggle = appShellStore((store) => store.toggleSidebar); + const open = appShellStore((store) => store.sidebarOpen); + return ; } export function Header({ hasSidebar = true }) { @@ -118,6 +117,7 @@ export function Header({ hasSidebar = true }) { return (

)}
- {todos?.reverse().map((todo) => ( -
-
- { - await $q.update({ done: !todo.done }, todo.id); + {todos && + [...todos].reverse().map((todo) => ( +
+
+ { + await $q.update({ done: !todo.done }, todo.id); + }} + /> +
{todo.title}
+
+
- -
- ))} + ))}
*/}
+ ); } + +function Debug({ app }: { app: App }) { + const [info, setInfo] = useState(); + const connection = app.em.connection as SQLocalConnection; + + useEffect(() => { + (async () => { + setInfo(await connection.client.getDatabaseInfo()); + app.emgr.onAny( + async () => { + setInfo(await connection.client.getDatabaseInfo()); + }, + { mode: "sync", id: "debug" }, + ); + })(); + }, []); + + async function download() { + const databaseFile = await connection.client.getDatabaseFile(); + const fileUrl = URL.createObjectURL(databaseFile); + + const a = document.createElement("a"); + a.href = fileUrl; + a.download = "database.sqlite3"; + a.click(); + a.remove(); + + URL.revokeObjectURL(fileUrl); + } + + return ( +
+ +
{JSON.stringify(info, null, 2)}
+
+ ); +} From 7b8c7f1ae494e119ec622772bee2ae6a39e47601 Mon Sep 17 00:00:00 2001 From: dswbx Date: Fri, 21 Mar 2025 18:01:44 +0100 Subject: [PATCH 38/42] fix s3 media upload in node environments by adding content length to request --- app/src/media/storage/adapters/StorageS3Adapter.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/src/media/storage/adapters/StorageS3Adapter.ts b/app/src/media/storage/adapters/StorageS3Adapter.ts index c5bfe78..37c651d 100644 --- a/app/src/media/storage/adapters/StorageS3Adapter.ts +++ b/app/src/media/storage/adapters/StorageS3Adapter.ts @@ -118,14 +118,20 @@ export class StorageS3Adapter extends AwsClient implements StorageAdapter { const res = await this.fetch(url, { method: "PUT", body, + headers: isFile(body) + ? { + // required for node environments + "Content-Length": String(body.size), + } + : {}, }); - if (res.ok) { - // "df20fcb574dba1446cf5ec997940492b" - return String(res.headers.get("etag")); + if (!res.ok) { + throw new Error(`Failed to upload object: ${res.status} ${res.statusText}`); } - return undefined; + // "df20fcb574dba1446cf5ec997940492b" + return String(res.headers.get("etag")); } private async headObject( From 0c15ec1434c6fb79da4a00f76e8639f0e3ed01ba Mon Sep 17 00:00:00 2001 From: dswbx Date: Fri, 21 Mar 2025 18:03:45 +0100 Subject: [PATCH 39/42] hide oauth client details with type password --- app/src/ui/routes/auth/auth.strategies.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/ui/routes/auth/auth.strategies.tsx b/app/src/ui/routes/auth/auth.strategies.tsx index 0e65f2c..24a23b2 100644 --- a/app/src/ui/routes/auth/auth.strategies.tsx +++ b/app/src/ui/routes/auth/auth.strategies.tsx @@ -240,8 +240,8 @@ const StrategyPasswordForm = () => { const StrategyOAuthForm = () => { return ( <> - - + + ); }; From 67e0374c04aef4b29fa69f55beae217ca9004805 Mon Sep 17 00:00:00 2001 From: dswbx Date: Fri, 21 Mar 2025 19:32:24 +0100 Subject: [PATCH 40/42] add change set to mutator insert/update after event --- app/package.json | 2 +- app/src/data/entities/Mutator.ts | 11 +++++++++-- app/src/data/events/index.ts | 7 ++++++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/package.json b/app/package.json index 364daf5..33a2356 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "type": "module", "sideEffects": false, "bin": "./dist/cli/index.js", - "version": "0.10.0-rc.5", + "version": "0.10.0-rc.7", "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/data/entities/Mutator.ts b/app/src/data/entities/Mutator.ts index fcac3ef..ce6330a 100644 --- a/app/src/data/entities/Mutator.ts +++ b/app/src/data/entities/Mutator.ts @@ -167,7 +167,9 @@ export class Mutator< const res = await this.single(query); - await this.emgr.emit(new Mutator.Events.MutatorInsertAfter({ entity, data: res.data })); + await this.emgr.emit( + new Mutator.Events.MutatorInsertAfter({ entity, data: res.data, changed: validatedData }), + ); return res as any; } @@ -198,7 +200,12 @@ export class Mutator< const res = await this.single(query); await this.emgr.emit( - new Mutator.Events.MutatorUpdateAfter({ entity, entityId: id, data: res.data }), + new Mutator.Events.MutatorUpdateAfter({ + entity, + entityId: id, + data: res.data, + changed: validatedData, + }), ); return res as any; diff --git a/app/src/data/events/index.ts b/app/src/data/events/index.ts index 5245fca..b9d7559 100644 --- a/app/src/data/events/index.ts +++ b/app/src/data/events/index.ts @@ -18,7 +18,11 @@ export class MutatorInsertBefore extends Event<{ entity: Entity; data: EntityDat }); } } -export class MutatorInsertAfter extends Event<{ entity: Entity; data: EntityData }> { +export class MutatorInsertAfter extends Event<{ + entity: Entity; + data: EntityData; + changed: EntityData; +}> { static override slug = "mutator-insert-after"; } export class MutatorUpdateBefore extends Event< @@ -48,6 +52,7 @@ export class MutatorUpdateAfter extends Event<{ entity: Entity; entityId: PrimaryFieldType; data: EntityData; + changed: EntityData; }> { static override slug = "mutator-update-after"; } From ec015b78494651af4d0cc5bd0bfdf414dcb4e58d Mon Sep 17 00:00:00 2001 From: dswbx Date: Tue, 25 Mar 2025 13:02:09 +0100 Subject: [PATCH 41/42] docs: add postgres and sqlocal instructions --- app/src/ui/hooks/use-event.ts | 17 ++++------ docs/usage/database.mdx | 63 ++++++++++++++++++++++++++++++++--- 2 files changed, 65 insertions(+), 15 deletions(-) diff --git a/app/src/ui/hooks/use-event.ts b/app/src/ui/hooks/use-event.ts index 26c39e3..23f8130 100644 --- a/app/src/ui/hooks/use-event.ts +++ b/app/src/ui/hooks/use-event.ts @@ -4,15 +4,12 @@ // there is no lifecycle or Hook in React that we can use to switch // .current at the right timing." // So we will have to make do with this "close enough" approach for now. -import { useEffect, useRef } from "react"; +import { useLayoutEffect, useRef } from "react"; +import { isDebug } from "core"; -export const useEvent = (fn: Fn | ((...args: any[]) => any) | undefined): Fn => { - const ref = useRef([fn, (...args) => ref[0](...args)]).current; - // Per Dan Abramov: useInsertionEffect executes marginally closer to the - // correct timing for ref synchronization than useLayoutEffect on React 18. - // See: https://github.com/facebook/react/pull/25881#issuecomment-1356244360 - useEffect(() => { - ref[0] = fn; - }, []); - return ref[1]; +export const useEvent = (fn: Fn): Fn => { + if (isDebug()) { + console.warn("useEvent() is deprecated"); + } + return fn; }; diff --git a/docs/usage/database.mdx b/docs/usage/database.mdx index 638abd9..0586572 100644 --- a/docs/usage/database.mdx +++ b/docs/usage/database.mdx @@ -55,12 +55,65 @@ connection object to your new database: } ``` -### Custom Connection - - Follow the progress of custom connections on its [Github Issue](https://github.com/bknd-io/bknd/issues/24). - If you're interested, make sure to upvote so it can be prioritized. - +### Cloudflare D1 +Using the [Cloudflare Adapter](/integration/cloudflare), you can choose to use a D1 database binding. To do so, you only need to add a D1 database to your `wrangler.toml` and it'll pick up automatically. To manually specify which D1 database to take, you can specify it manually: +```ts +import { serve, d1 } from "bknd/adapter/cloudflare"; + +export default serve({ + app: ({ env }) => d1({ binding: env.D1_BINDING }) +}); +``` + +### PostgreSQL +To use bknd with Postgres, you need to install the `@bknd/postgres` package. You can do so by running the following command: + +```bash +npm install @bknd/postgres +``` + +This package uses `pg` under the hood. If you'd like to see `postgres` or any other flavor, please create an [issue on Github](https://github.com/bknd-io/bknd/issues/new). + +To establish a connection to your database, you can use any connection options available on the [`pg`](https://node-postgres.com/apis/client) package. Here is a quick example using the [Node.js Adapter](http://localhost:3000/integration/node): + +```js +import { serve } from "bknd/adapter/node"; +import { PostgresConnection } from "@bknd/postgres"; + +/** @type {import("bknd/adapter/node").NodeBkndConfig} */ +const config = { + connection: new PostgresConnection({ + connectionString: + "postgresql://user:password@localhost:5432/database", + }), +}; + +serve(config); +``` + +### SQLocal +To use bknd with `sqlocal` for a offline expierence, you need to install the `@bknd/sqlocal` package. You can do so by running the following command: + +```bash +npm install @bknd/sqlocal +``` + +This package uses `sqlocal` under the hood. Consult the [sqlocal documentation](https://sqlocal.dallashoffman.com/guide/setup) for connection options: + +```js +import { createApp } from "bknd"; +import { SQLocalConnection } from "@bknd/sqlocal"; + +const app = createApp({ + connection: new SQLocalConnection({ + databasePath: ":localStorage:", + verbose: true, + }) +}); +``` + +### Custom Connection Any bknd app instantiation accepts as connection either `undefined`, a connection object like described above, or an class instance that extends from `Connection`: From f8f5ef9c989b496f800537bb6f8751e4d3aaf514 Mon Sep 17 00:00:00 2001 From: dswbx Date: Tue, 25 Mar 2025 13:03:15 +0100 Subject: [PATCH 42/42] bump version to 0.10 --- app/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/package.json b/app/package.json index 33a2356..445cc09 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "type": "module", "sideEffects": false, "bin": "./dist/cli/index.js", - "version": "0.10.0-rc.7", + "version": "0.10.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": {