diff --git a/app/package.json b/app/package.json index f0320ec..28622f9 100644 --- a/app/package.json +++ b/app/package.json @@ -36,6 +36,16 @@ "aws4fetch": "^1.0.18" }, "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", "@mantine/core": "^7.13.4", "@mantine/hooks": "^7.13.4", @@ -45,41 +55,32 @@ "@rjsf/core": "^5.22.2", "@tabler/icons-react": "3.18.0", "@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", "@types/node": "^22.10.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", + "@uiw/react-codemirror": "^4.23.6", "@vitejs/plugin-react": "^4.3.3", + "@xyflow/react": "^12.3.2", "autoprefixer": "^10.4.20", "esbuild-postcss": "^0.0.4", + "jotai": "^2.10.1", + "open": "^10.1.0", "openapi-types": "^12.1.3", "postcss": "^8.4.47", "postcss-preset-mantine": "^1.17.0", "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-animate": "^1.0.7", "tsup": "^8.3.5", "vite": "^5.4.10", "vite-plugin-static-copy": "^2.0.0", - "vite-tsconfig-paths": "^5.0.1" + "vite-tsconfig-paths": "^5.0.1", + "wouter": "^3.3.5" }, "optionalDependencies": { "@hono/node-server": "^1.13.7" diff --git a/app/src/App.ts b/app/src/App.ts index 5c6ab2c..15968c1 100644 --- a/app/src/App.ts +++ b/app/src/App.ts @@ -56,29 +56,6 @@ export class App { 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() { return this.modules.ctx().emgr; } @@ -149,4 +126,31 @@ export class App { toJSON(secrets?: boolean) { 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); } diff --git a/app/src/adapter/astro/astro.adapter.ts b/app/src/adapter/astro/astro.adapter.ts index 71ad9f6..f86410a 100644 --- a/app/src/adapter/astro/astro.adapter.ts +++ b/app/src/adapter/astro/astro.adapter.ts @@ -1,5 +1,4 @@ -import { Api, type ApiOptions } from "bknd"; -import { App, type CreateAppConfig } from "bknd"; +import { Api, type ApiOptions, App, type CreateAppConfig } from "bknd"; type TAstro = { request: Request; diff --git a/app/src/adapter/bun/bun.adapter.ts b/app/src/adapter/bun/bun.adapter.ts index 9c62047..ae14be5 100644 --- a/app/src/adapter/bun/bun.adapter.ts +++ b/app/src/adapter/bun/bun.adapter.ts @@ -1,55 +1,59 @@ +/// + import path from "node:path"; import { App, type CreateAppConfig } from "bknd"; -import { LibsqlConnection } from "bknd/data"; +import type { Serve, ServeOptions } from "bun"; import { serveStatic } from "hono/bun"; -async function getConnection(conn?: CreateAppConfig["connection"]) { - 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 function serve(_config: Partial = {}, distPath?: string) { +let app: App; +export async function createApp(_config: Partial = {}, distPath?: string) { const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static"); - let app: App; - return async (req: Request) => { - if (!app) { - const connection = await getConnection(_config.connection); - app = App.create({ - ..._config, - connection - }); + if (!app) { + app = App.create(_config); - app.emgr.on( - "app-built", - async () => { - app.modules.server.get( - "/*", - serveStatic({ - root - }) - ); - app.registerAdminController(); - }, - "sync" - ); + app.emgr.on( + "app-built", + async () => { + app.modules.server.get( + "/*", + serveStatic({ + root + }) + ); + app.registerAdminController(); + }, + "sync" + ); - await app.build(); - } + await app.build(); + } - return app.fetch(req); - }; + return app; +} + +export type BunAdapterOptions = Omit & + 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}`); } diff --git a/app/src/adapter/node/index.ts b/app/src/adapter/node/index.ts index 5360ddb..47d4c97 100644 --- a/app/src/adapter/node/index.ts +++ b/app/src/adapter/node/index.ts @@ -2,51 +2,34 @@ import path from "node:path"; import { serve as honoServe } from "@hono/node-server"; import { serveStatic } from "@hono/node-server/serve-static"; import { App, type CreateAppConfig } from "bknd"; -import { LibsqlConnection } from "bknd/data"; -async function getConnection(conn?: CreateAppConfig["connection"]) { - 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 = { +export type NodeAdapterOptions = CreateAppConfig & { relativeDistPath?: string; port?: number; hostname?: string; listener?: Parameters[1]; }; -export function serve(_config: Partial = {}, options: NodeAdapterOptions = {}) { +export function serve({ + relativeDistPath, + port = 1337, + hostname, + listener, + ...config +}: NodeAdapterOptions = {}) { const root = path.relative( process.cwd(), - path.resolve(options.relativeDistPath ?? "./node_modules/bknd/dist", "static") + path.resolve(relativeDistPath ?? "./node_modules/bknd/dist", "static") ); let app: App; honoServe( { - port: options.port ?? 1337, - hostname: options.hostname, + port, + hostname, fetch: async (req: Request) => { if (!app) { - const connection = await getConnection(_config.connection); - app = App.create({ - ..._config, - connection - }); + app = App.create(config); app.emgr.on( "app-built", @@ -68,6 +51,9 @@ export function serve(_config: Partial = {}, options: NodeAdapt return app.fetch(req); } }, - options.listener + (connInfo) => { + console.log(`Server is running on http://localhost:${connInfo.port}`); + listener?.(connInfo); + } ); } diff --git a/app/src/cli/commands/run/platform.ts b/app/src/cli/commands/run/platform.ts index 46a725b..309df7f 100644 --- a/app/src/cli/commands/run/platform.ts +++ b/app/src/cli/commands/run/platform.ts @@ -1,10 +1,8 @@ -import { readFile } from "node:fs/promises"; import path from "node:path"; -import type { ServeStaticOptions } from "@hono/node-server/serve-static"; -import { type Config, createClient } from "@libsql/client/node"; -import { Connection, LibsqlConnection, SqliteLocalConnection } from "data"; +import type { Config } from "@libsql/client/node"; 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 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 }) { const port = options.port; - console.log("running on", server, port); + console.log(`(using ${server} serve)`); + switch (server) { case "node": { // 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); -} - -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:")); + const url = `http://localhost:${port}`; + console.log(`Server listening on ${url}`); + await open(url); } export async function getConfigPath(filePath?: string) { diff --git a/app/src/cli/commands/run/run.ts b/app/src/cli/commands/run/run.ts index 0663f2f..7ad9568 100644 --- a/app/src/cli/commands/run/run.ts +++ b/app/src/cli/commands/run/run.ts @@ -1,16 +1,13 @@ import type { Config } from "@libsql/client/node"; -import { App } from "App"; +import { App, type CreateAppConfig } from "App"; import type { BkndConfig } from "adapter"; import type { CliCommand } from "cli/types"; import { Option } from "commander"; -import type { Connection } from "data"; import { PLATFORMS, type Platform, attachServeStatic, getConfigPath, - getConnection, - getHtml, startServer } from "./platform"; @@ -41,14 +38,14 @@ export const run: CliCommand = (program) => { }; type MakeAppConfig = { - connection: Connection; + connection?: CreateAppConfig["connection"]; server?: { platform?: Platform }; setAdminHtml?: boolean; onBuilt?: (app: App) => Promise; }; async function makeApp(config: MakeAppConfig) { - const app = new App(config.connection); + const app = App.create({ connection: config.connection }); app.emgr.on( "app-built", @@ -99,9 +96,9 @@ async function action(options: { let app: App; if (options.dbUrl || !configFilePath) { - const connection = getConnection( - options.dbUrl ? { url: options.dbUrl, authToken: options.dbToken } : undefined - ); + const connection = options.dbUrl + ? { type: "libsql" as const, config: { url: options.dbUrl, authToken: options.dbToken } } + : undefined; app = await makeApp({ connection, server: { platform: options.server } }); } else { console.log("Using config from:", configFilePath); diff --git a/app/src/data/connection/Connection.ts b/app/src/data/connection/Connection.ts index bc97ff0..e4ce455 100644 --- a/app/src/data/connection/Connection.ts +++ b/app/src/data/connection/Connection.ts @@ -41,16 +41,18 @@ export type DbFunctions = { >; }; -export abstract class Connection { - cls = "bknd:connection"; - kysely: Kysely; +const CONN_SYMBOL = Symbol.for("bknd:connection"); + +export abstract class Connection { + kysely: Kysely; constructor( - kysely: Kysely, + kysely: Kysely, public fn: Partial = {}, protected plugins: KyselyPlugin[] = [] ) { this.kysely = kysely; + this[CONN_SYMBOL] = true; } /** @@ -58,8 +60,9 @@ export abstract class Connection { * coming from different places * @param conn */ - static isConnection(conn: any): conn is Connection { - return conn?.cls === "bknd:connection"; + static isConnection(conn: unknown): conn is Connection { + if (!conn) return false; + return conn[CONN_SYMBOL] === true; } getIntrospector(): ConnectionIntrospector { diff --git a/app/src/index.ts b/app/src/index.ts index 649bf71..578ab5d 100644 --- a/app/src/index.ts +++ b/app/src/index.ts @@ -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 { getDefaultConfig, getDefaultSchema, diff --git a/bun.lockb b/bun.lockb index 7adde63..22d6785 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docs/integration/bun.mdx b/docs/integration/bun.mdx index bfec760..8c4a8ec 100644 --- a/docs/integration/bun.mdx +++ b/docs/integration/bun.mdx @@ -16,7 +16,8 @@ the admin panel. // index.ts import { serve } from "bknd/adapter/bun"; -const handler = serve({ +// if the configuration is omitted, it uses an in-memory database +serve({ connection: { type: "libsql", 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. diff --git a/docs/integration/node.mdx b/docs/integration/node.mdx new file mode 100644 index 0000000..d089bf1 --- /dev/null +++ b/docs/integration/node.mdx @@ -0,0 +1,37 @@ +--- +title: 'Node' +description: 'Run bknd inside Node' +--- +import InstallBknd from '/snippets/install-bknd.mdx'; + +## Installation +Install bknd as a dependency: + + +## 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 +``` \ No newline at end of file diff --git a/docs/mint.json b/docs/mint.json index 634e104..ac740cb 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -104,7 +104,7 @@ "integration/vite", "integration/express", "integration/astro", - "integration/nodejs", + "integration/node", "integration/deno", "integration/browser" ] diff --git a/docs/setup.mdx b/docs/setup.mdx index 357f40e..8382bfd 100644 --- a/docs/setup.mdx +++ b/docs/setup.mdx @@ -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: ```json { - "type": "sqlite", + "type": "libsql", "config": { - "file": "path/to/your/database.db" + "url": "file:" } } ``` @@ -56,6 +56,30 @@ connection object to your new database: } ``` +### Custom Connection (unstable) + + 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. + +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 To install **bknd**, run the following command: \ No newline at end of file diff --git a/examples/bun/index.ts b/examples/bun/index.ts index 2d2b9cd..8e8e4ac 100644 --- a/examples/bun/index.ts +++ b/examples/bun/index.ts @@ -1,26 +1,20 @@ // @ts-ignore somehow causes types:build issues on app -import type { CreateAppConfig } from "bknd"; -// @ts-ignore somehow causes types:build issues on app -import { serve } from "bknd/adapter/bun"; +import { type BunAdapterOptions, 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 -const config = { +const config: BunAdapterOptions = { connection: { type: "libsql", 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({ - 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"); +serve(config); diff --git a/examples/node/index.js b/examples/node/index.js index c16211a..faa1fef 100644 --- a/examples/node/index.js +++ b/examples/node/index.js @@ -1,22 +1,20 @@ 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 -/** @type {import("bknd").CreateAppConfig} */ +/** @type {import("bknd/adapter/node").NodeAdapterOptions} */ const config = { connection: { type: "libsql", 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 // leave blank if you're running this from a different project relativeDistPath: "../../app/dist" -}); +}; + +serve(config);