small refactorings and cleanups, improved bun/node adapter, updated docs

This commit is contained in:
dswbx
2024-12-07 18:55:02 +01:00
parent 154703f873
commit 94cc4042d3
16 changed files with 224 additions and 203 deletions

View File

@@ -36,6 +36,16 @@
"aws4fetch": "^1.0.18" "aws4fetch": "^1.0.18"
}, },
"devDependencies": { "devDependencies": {
"@aws-sdk/client-s3": "^3.613.0",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-liquid": "^6.2.1",
"@dagrejs/dagre": "^1.1.4",
"@hello-pangea/dnd": "^17.0.0",
"@hono/typebox-validator": "^0.2.6",
"@hono/vite-dev-server": "^0.17.0",
"@hono/zod-validator": "^0.4.1",
"@hookform/resolvers": "^3.9.1",
"@libsql/kysely-libsql": "^0.4.1", "@libsql/kysely-libsql": "^0.4.1",
"@mantine/core": "^7.13.4", "@mantine/core": "^7.13.4",
"@mantine/hooks": "^7.13.4", "@mantine/hooks": "^7.13.4",
@@ -45,41 +55,32 @@
"@rjsf/core": "^5.22.2", "@rjsf/core": "^5.22.2",
"@tabler/icons-react": "3.18.0", "@tabler/icons-react": "3.18.0",
"@tanstack/react-query": "^5.59.16", "@tanstack/react-query": "^5.59.16",
"@uiw/react-codemirror": "^4.23.6",
"@xyflow/react": "^12.3.2",
"jotai": "^2.10.1",
"react-hook-form": "^7.53.1",
"react-icons": "5.2.1",
"react-json-view-lite": "^2.0.1",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"wouter": "^3.3.5",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-liquid": "^6.2.1",
"@dagrejs/dagre": "^1.1.4",
"@hello-pangea/dnd": "^17.0.0",
"@hono/typebox-validator": "^0.2.6",
"@hono/zod-validator": "^0.4.1",
"@hookform/resolvers": "^3.9.1",
"@aws-sdk/client-s3": "^3.613.0",
"@hono/vite-dev-server": "^0.17.0",
"@tanstack/react-query-devtools": "^5.59.16", "@tanstack/react-query-devtools": "^5.59.16",
"@types/node": "^22.10.0", "@types/node": "^22.10.0",
"@types/react": "^18.3.12", "@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.1",
"@uiw/react-codemirror": "^4.23.6",
"@vitejs/plugin-react": "^4.3.3", "@vitejs/plugin-react": "^4.3.3",
"@xyflow/react": "^12.3.2",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"esbuild-postcss": "^0.0.4", "esbuild-postcss": "^0.0.4",
"jotai": "^2.10.1",
"open": "^10.1.0",
"openapi-types": "^12.1.3", "openapi-types": "^12.1.3",
"postcss": "^8.4.47", "postcss": "^8.4.47",
"postcss-preset-mantine": "^1.17.0", "postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1", "postcss-simple-vars": "^7.0.1",
"react-hook-form": "^7.53.1",
"react-icons": "5.2.1",
"react-json-view-lite": "^2.0.1",
"tailwind-merge": "^2.5.4",
"tailwindcss": "^3.4.14", "tailwindcss": "^3.4.14",
"tailwindcss-animate": "^1.0.7",
"tsup": "^8.3.5", "tsup": "^8.3.5",
"vite": "^5.4.10", "vite": "^5.4.10",
"vite-plugin-static-copy": "^2.0.0", "vite-plugin-static-copy": "^2.0.0",
"vite-tsconfig-paths": "^5.0.1" "vite-tsconfig-paths": "^5.0.1",
"wouter": "^3.3.5"
}, },
"optionalDependencies": { "optionalDependencies": {
"@hono/node-server": "^1.13.7" "@hono/node-server": "^1.13.7"

View File

@@ -56,29 +56,6 @@ export class App<DB = any> {
this.modules.ctx().emgr.registerEvents(AppEvents); this.modules.ctx().emgr.registerEvents(AppEvents);
} }
static create(config: CreateAppConfig) {
let connection: Connection | undefined = undefined;
try {
if (Connection.isConnection(config.connection)) {
connection = config.connection;
} else if (typeof config.connection === "object") {
connection = new LibsqlConnection(config.connection.config);
} else {
connection = new LibsqlConnection({ url: ":memory:" });
console.warn("[!] No connection provided, using in-memory database");
}
} catch (e) {
console.error("Could not create connection", e);
}
if (!connection) {
throw new Error("Invalid connection");
}
return new App(connection, config.initialConfig, config.plugins, config.options);
}
get emgr() { get emgr() {
return this.modules.ctx().emgr; return this.modules.ctx().emgr;
} }
@@ -149,4 +126,31 @@ export class App<DB = any> {
toJSON(secrets?: boolean) { toJSON(secrets?: boolean) {
return this.modules.toJSON(secrets); return this.modules.toJSON(secrets);
} }
static create(config: CreateAppConfig) {
return createApp(config);
}
}
export function createApp(config: CreateAppConfig) {
let connection: Connection | undefined = undefined;
try {
if (Connection.isConnection(config.connection)) {
connection = config.connection;
} else if (typeof config.connection === "object") {
connection = new LibsqlConnection(config.connection.config);
} else {
connection = new LibsqlConnection({ url: ":memory:" });
console.warn("[!] No connection provided, using in-memory database");
}
} catch (e) {
console.error("Could not create connection", e);
}
if (!connection) {
throw new Error("Invalid connection");
}
return new App(connection, config.initialConfig, config.plugins, config.options);
} }

View File

@@ -1,5 +1,4 @@
import { Api, type ApiOptions } from "bknd"; import { Api, type ApiOptions, App, type CreateAppConfig } from "bknd";
import { App, type CreateAppConfig } from "bknd";
type TAstro = { type TAstro = {
request: Request; request: Request;

View File

@@ -1,55 +1,59 @@
/// <reference types="bun-types" />
import path from "node:path"; import path from "node:path";
import { App, type CreateAppConfig } from "bknd"; import { App, type CreateAppConfig } from "bknd";
import { LibsqlConnection } from "bknd/data"; import type { Serve, ServeOptions } from "bun";
import { serveStatic } from "hono/bun"; import { serveStatic } from "hono/bun";
async function getConnection(conn?: CreateAppConfig["connection"]) { let app: App;
if (conn) { export async function createApp(_config: Partial<CreateAppConfig> = {}, distPath?: string) {
if (LibsqlConnection.isConnection(conn)) {
return conn;
}
return new LibsqlConnection(conn.config);
}
const createClient = await import("@libsql/client/node").then((m) => m.createClient);
if (!createClient) {
throw new Error('libsql client not found, you need to install "@libsql/client/node"');
}
console.log("Using in-memory database");
return new LibsqlConnection(createClient({ url: ":memory:" }));
}
export function serve(_config: Partial<CreateAppConfig> = {}, distPath?: string) {
const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static"); const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static");
let app: App;
return async (req: Request) => { if (!app) {
if (!app) { app = App.create(_config);
const connection = await getConnection(_config.connection);
app = App.create({
..._config,
connection
});
app.emgr.on( app.emgr.on(
"app-built", "app-built",
async () => { async () => {
app.modules.server.get( app.modules.server.get(
"/*", "/*",
serveStatic({ serveStatic({
root root
}) })
); );
app.registerAdminController(); app.registerAdminController();
}, },
"sync" "sync"
); );
await app.build(); await app.build();
} }
return app.fetch(req); return app;
}; }
export type BunAdapterOptions = Omit<ServeOptions, "fetch"> &
CreateAppConfig & {
distPath?: string;
};
export function serve({
distPath,
connection,
initialConfig,
plugins,
options,
port = 1337,
...serveOptions
}: BunAdapterOptions = {}) {
Bun.serve({
...serveOptions,
port,
fetch: async (request: Request) => {
const app = await createApp({ connection, initialConfig, plugins, options }, distPath);
return app.fetch(request);
}
});
console.log(`Server is running on http://localhost:${port}`);
} }

View File

@@ -2,51 +2,34 @@ import path from "node:path";
import { serve as honoServe } from "@hono/node-server"; import { serve as honoServe } from "@hono/node-server";
import { serveStatic } from "@hono/node-server/serve-static"; import { serveStatic } from "@hono/node-server/serve-static";
import { App, type CreateAppConfig } from "bknd"; import { App, type CreateAppConfig } from "bknd";
import { LibsqlConnection } from "bknd/data";
async function getConnection(conn?: CreateAppConfig["connection"]) { export type NodeAdapterOptions = CreateAppConfig & {
if (conn) {
if (LibsqlConnection.isConnection(conn)) {
return conn;
}
return new LibsqlConnection(conn.config);
}
const createClient = await import("@libsql/client/node").then((m) => m.createClient);
if (!createClient) {
throw new Error('libsql client not found, you need to install "@libsql/client/node"');
}
console.log("Using in-memory database");
return new LibsqlConnection(createClient({ url: ":memory:" }));
}
export type NodeAdapterOptions = {
relativeDistPath?: string; relativeDistPath?: string;
port?: number; port?: number;
hostname?: string; hostname?: string;
listener?: Parameters<typeof honoServe>[1]; listener?: Parameters<typeof honoServe>[1];
}; };
export function serve(_config: Partial<CreateAppConfig> = {}, options: NodeAdapterOptions = {}) { export function serve({
relativeDistPath,
port = 1337,
hostname,
listener,
...config
}: NodeAdapterOptions = {}) {
const root = path.relative( const root = path.relative(
process.cwd(), process.cwd(),
path.resolve(options.relativeDistPath ?? "./node_modules/bknd/dist", "static") path.resolve(relativeDistPath ?? "./node_modules/bknd/dist", "static")
); );
let app: App; let app: App;
honoServe( honoServe(
{ {
port: options.port ?? 1337, port,
hostname: options.hostname, hostname,
fetch: async (req: Request) => { fetch: async (req: Request) => {
if (!app) { if (!app) {
const connection = await getConnection(_config.connection); app = App.create(config);
app = App.create({
..._config,
connection
});
app.emgr.on( app.emgr.on(
"app-built", "app-built",
@@ -68,6 +51,9 @@ export function serve(_config: Partial<CreateAppConfig> = {}, options: NodeAdapt
return app.fetch(req); return app.fetch(req);
} }
}, },
options.listener (connInfo) => {
console.log(`Server is running on http://localhost:${connInfo.port}`);
listener?.(connInfo);
}
); );
} }

View File

@@ -1,10 +1,8 @@
import { readFile } from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import type { ServeStaticOptions } from "@hono/node-server/serve-static"; import type { Config } from "@libsql/client/node";
import { type Config, createClient } from "@libsql/client/node";
import { Connection, LibsqlConnection, SqliteLocalConnection } from "data";
import type { MiddlewareHandler } from "hono"; import type { MiddlewareHandler } from "hono";
import { fileExists, getDistPath, getRelativeDistPath } from "../../utils/sys"; import open from "open";
import { fileExists, getRelativeDistPath } from "../../utils/sys";
export const PLATFORMS = ["node", "bun"] as const; export const PLATFORMS = ["node", "bun"] as const;
export type Platform = (typeof PLATFORMS)[number]; export type Platform = (typeof PLATFORMS)[number];
@@ -33,7 +31,8 @@ export async function attachServeStatic(app: any, platform: Platform) {
export async function startServer(server: Platform, app: any, options: { port: number }) { export async function startServer(server: Platform, app: any, options: { port: number }) {
const port = options.port; const port = options.port;
console.log("running on", server, port); console.log(`(using ${server} serve)`);
switch (server) { switch (server) {
case "node": { case "node": {
// https://github.com/honojs/node-server/blob/main/src/response.ts#L88 // https://github.com/honojs/node-server/blob/main/src/response.ts#L88
@@ -53,27 +52,9 @@ export async function startServer(server: Platform, app: any, options: { port: n
} }
} }
console.log("Server listening on", "http://localhost:" + port); const url = `http://localhost:${port}`;
} console.log(`Server listening on ${url}`);
await open(url);
export async function getHtml() {
return await readFile(path.resolve(getDistPath(), "static/index.html"), "utf-8");
}
export function getConnection(connectionOrConfig?: Connection | Config): Connection {
if (connectionOrConfig) {
if (connectionOrConfig instanceof Connection) {
return connectionOrConfig;
}
if ("url" in connectionOrConfig) {
return new LibsqlConnection(createClient(connectionOrConfig));
}
}
console.log("Using in-memory database");
return new LibsqlConnection(createClient({ url: ":memory:" }));
//return new SqliteLocalConnection(new Database(":memory:"));
} }
export async function getConfigPath(filePath?: string) { export async function getConfigPath(filePath?: string) {

View File

@@ -1,16 +1,13 @@
import type { Config } from "@libsql/client/node"; import type { Config } from "@libsql/client/node";
import { App } from "App"; import { App, type CreateAppConfig } from "App";
import type { BkndConfig } from "adapter"; import type { BkndConfig } from "adapter";
import type { CliCommand } from "cli/types"; import type { CliCommand } from "cli/types";
import { Option } from "commander"; import { Option } from "commander";
import type { Connection } from "data";
import { import {
PLATFORMS, PLATFORMS,
type Platform, type Platform,
attachServeStatic, attachServeStatic,
getConfigPath, getConfigPath,
getConnection,
getHtml,
startServer startServer
} from "./platform"; } from "./platform";
@@ -41,14 +38,14 @@ export const run: CliCommand = (program) => {
}; };
type MakeAppConfig = { type MakeAppConfig = {
connection: Connection; connection?: CreateAppConfig["connection"];
server?: { platform?: Platform }; server?: { platform?: Platform };
setAdminHtml?: boolean; setAdminHtml?: boolean;
onBuilt?: (app: App) => Promise<void>; onBuilt?: (app: App) => Promise<void>;
}; };
async function makeApp(config: MakeAppConfig) { async function makeApp(config: MakeAppConfig) {
const app = new App(config.connection); const app = App.create({ connection: config.connection });
app.emgr.on( app.emgr.on(
"app-built", "app-built",
@@ -99,9 +96,9 @@ async function action(options: {
let app: App; let app: App;
if (options.dbUrl || !configFilePath) { if (options.dbUrl || !configFilePath) {
const connection = getConnection( const connection = options.dbUrl
options.dbUrl ? { url: options.dbUrl, authToken: options.dbToken } : undefined ? { type: "libsql" as const, config: { url: options.dbUrl, authToken: options.dbToken } }
); : undefined;
app = await makeApp({ connection, server: { platform: options.server } }); app = await makeApp({ connection, server: { platform: options.server } });
} else { } else {
console.log("Using config from:", configFilePath); console.log("Using config from:", configFilePath);

View File

@@ -41,16 +41,18 @@ export type DbFunctions = {
>; >;
}; };
export abstract class Connection { const CONN_SYMBOL = Symbol.for("bknd:connection");
cls = "bknd:connection";
kysely: Kysely<any>; export abstract class Connection<DB = any> {
kysely: Kysely<DB>;
constructor( constructor(
kysely: Kysely<any>, kysely: Kysely<DB>,
public fn: Partial<DbFunctions> = {}, public fn: Partial<DbFunctions> = {},
protected plugins: KyselyPlugin[] = [] protected plugins: KyselyPlugin[] = []
) { ) {
this.kysely = kysely; this.kysely = kysely;
this[CONN_SYMBOL] = true;
} }
/** /**
@@ -58,8 +60,9 @@ export abstract class Connection {
* coming from different places * coming from different places
* @param conn * @param conn
*/ */
static isConnection(conn: any): conn is Connection { static isConnection(conn: unknown): conn is Connection {
return conn?.cls === "bknd:connection"; if (!conn) return false;
return conn[CONN_SYMBOL] === true;
} }
getIntrospector(): ConnectionIntrospector { getIntrospector(): ConnectionIntrospector {

View File

@@ -1,6 +1,5 @@
export { App, type AppConfig, type CreateAppConfig } from "./App"; export { App, createApp, AppEvents, type AppConfig, type CreateAppConfig } from "./App";
export { MediaField } from "media/MediaField";
export { export {
getDefaultConfig, getDefaultConfig,
getDefaultSchema, getDefaultSchema,

BIN
bun.lockb

Binary file not shown.

View File

@@ -16,7 +16,8 @@ the admin panel.
// index.ts // index.ts
import { serve } from "bknd/adapter/bun"; import { serve } from "bknd/adapter/bun";
const handler = serve({ // if the configuration is omitted, it uses an in-memory database
serve({
connection: { connection: {
type: "libsql", type: "libsql",
config: { config: {
@@ -25,13 +26,6 @@ const handler = serve({
} }
} }
}); });
Bun.serve({
port: 1337,
fetch: handler
});
console.log("Server running at http://localhost:1337");
``` ```
For more information about the connection object, refer to the [Setup](/setup) guide. For more information about the connection object, refer to the [Setup](/setup) guide.

37
docs/integration/node.mdx Normal file
View File

@@ -0,0 +1,37 @@
---
title: 'Node'
description: 'Run bknd inside Node'
---
import InstallBknd from '/snippets/install-bknd.mdx';
## Installation
Install bknd as a dependency:
<InstallBknd />
## Serve the API & static files
The `serve` function of the Node adapter makes sure to also serve the static files required for
the admin panel.
``` tsx
// index.js
import { serve } from "bknd/adapter/node";
// if the configuration is omitted, it uses an in-memory database
/** @type {import("bknd/adapter/node").NodeAdapterOptions} */
const config = {
connection: {
type: "libsql",
config: {
url: ":memory:"
}
}
};
serve(config);
```
For more information about the connection object, refer to the [Setup](/setup) guide.
Run the application using node by executing:
```bash
node index.js
```

View File

@@ -104,7 +104,7 @@
"integration/vite", "integration/vite",
"integration/express", "integration/express",
"integration/astro", "integration/astro",
"integration/nodejs", "integration/node",
"integration/deno", "integration/deno",
"integration/browser" "integration/browser"
] ]

View File

@@ -18,9 +18,9 @@ The easiest to get started is using SQLite as a file. When serving the API in th
the function accepts an object with connection details. To use a file, use the following: the function accepts an object with connection details. To use a file, use the following:
```json ```json
{ {
"type": "sqlite", "type": "libsql",
"config": { "config": {
"file": "path/to/your/database.db" "url": "file:<path/to/your/database.db>"
} }
} }
``` ```
@@ -56,6 +56,30 @@ connection object to your new database:
} }
``` ```
### Custom Connection (unstable)
<Note>
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.
</Note>
Any bknd app instantiation accepts as connection either `undefined`, a connection object like
described above, or an class instance that extends from `Connection`:
```ts
import { createApp } from "bknd";
import { Connection } from "bknd/data";
class CustomConnection extends Connection {
constructor() {
const kysely = new Kysely(/* ... */);
super(kysely);
}
}
const connection = new CustomConnection();
// e.g. and then, create an instance
const app = createApp({ connection })
```
## Installation ## Installation
To install **bknd**, run the following command: To install **bknd**, run the following command:
<InstallBknd /> <InstallBknd />

View File

@@ -1,26 +1,20 @@
// @ts-ignore somehow causes types:build issues on app // @ts-ignore somehow causes types:build issues on app
import type { CreateAppConfig } from "bknd"; import { type BunAdapterOptions, serve } from "bknd/adapter/bun";
// @ts-ignore somehow causes types:build issues on app
import { serve } from "bknd/adapter/bun"; // Actually, all it takes is the following line:
// serve();
// this is optional, if omitted, it uses an in-memory database // this is optional, if omitted, it uses an in-memory database
const config = { const config: BunAdapterOptions = {
connection: { connection: {
type: "libsql", type: "libsql",
config: { config: {
url: "http://localhost:8080" url: ":memory:"
} }
} },
} satisfies CreateAppConfig; // this is only required to run inside the same workspace
// leave blank if you're running this from a different project
distPath: "../../app/dist"
};
Bun.serve({ serve(config);
port: 1337,
fetch: serve(
config,
// this is only required to run inside the same workspace
// leave blank if you're running this from a different project
"../../app/dist"
)
});
console.log("Server running at http://localhost:1337");

View File

@@ -1,22 +1,20 @@
import { serve } from "bknd/adapter/node"; import { serve } from "bknd/adapter/node";
// Actually, all it takes is the following line:
// serve();
// this is optional, if omitted, it uses an in-memory database // this is optional, if omitted, it uses an in-memory database
/** @type {import("bknd").CreateAppConfig} */ /** @type {import("bknd/adapter/node").NodeAdapterOptions} */
const config = { const config = {
connection: { connection: {
type: "libsql", type: "libsql",
config: { config: {
url: "http://localhost:8080" url: ":memory:"
} }
}
};
serve(config, {
port: 1337,
listener: ({ port }) => {
console.log(`Server is running on http://localhost:${port}`);
}, },
// this is only required to run inside the same workspace // this is only required to run inside the same workspace
// leave blank if you're running this from a different project // leave blank if you're running this from a different project
relativeDistPath: "../../app/dist" relativeDistPath: "../../app/dist"
}); };
serve(config);