Compare commits

..

8 Commits

65 changed files with 504 additions and 559 deletions
+4 -1
View File
@@ -1,7 +1,10 @@
{ {
"mcpServers": { "mcpServers": {
"bknd": { "bknd": {
"url": "http://localhost:28623/api/system/mcp" "url": "http://localhost:3000/mcp",
"headers": {
"API_KEY": "value"
}
} }
} }
} }
+4 -5
View File
@@ -3,7 +3,7 @@
![bknd](https://raw.githubusercontent.com/bknd-io/bknd/refs/heads/main/docs/public/assets/poster.png) ![bknd](https://raw.githubusercontent.com/bknd-io/bknd/refs/heads/main/docs/public/assets/poster.png)
<p align="center" width="100%"> <p align="center" width="100%">
<a href="https://stackblitz.com/github/bknd-io/bknd-demo?hideExplorer=1&embed=1&view=preview&initialPath=%2Fdata%2Fschema" target="_blank"> <a href="https://stackblitz.com/github/bknd-io/bknd-examples?hideExplorer=1&embed=1&view=preview&startScript=example-admin-rich&initialPath=%2Fdata%2Fschema" target="_blank">
<strong>⭐ Live Demo</strong> <strong>⭐ Live Demo</strong>
</a> </a>
</p> </p>
@@ -19,7 +19,7 @@ bknd simplifies app development by providing a fully functional backend for data
**For documentation and examples, please visit https://docs.bknd.io.** **For documentation and examples, please visit https://docs.bknd.io.**
> [!WARNING] > [!WARNING]
> This project requires Node.js 22.13 or higher (because of `node:sqlite`). > This project requires Node.js 22 or higher (because of `node:sqlite`).
> >
> Please keep in mind that **bknd** is still under active development > Please keep in mind that **bknd** is still under active development
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0. > and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
@@ -27,8 +27,8 @@ bknd simplifies app development by providing a fully functional backend for data
## Size ## Size
![gzipped size of bknd](https://img.shields.io/bundlejs/size/bknd?label=bknd) ![gzipped size of bknd](https://img.shields.io/bundlejs/size/bknd?label=bknd)
![gzipped size of bknd/client](https://img.badgesize.io/https://unpkg.com/bknd@latest/dist/ui/client/index.js?compression=gzip&label=bknd/client) ![gzipped size of bknd/client](https://img.badgesize.io/https://unpkg.com/bknd@latest/dist/ui/client/index.js?compression=gzip&label=bknd/client)
![gzipped size of bknd/elements](https://img.badgesize.io/https://unpkg.com/bknd@latest/dist/ui/elements/index.js?compression=gzip&label=bknd/elements&t=1) ![gzipped size of bknd/elements](https://img.badgesize.io/https://unpkg.com/bknd@latest/dist/ui/elements/index.js?compression=gzip&label=bknd/elements)
![gzipped size of bknd/ui](https://img.badgesize.io/https://unpkg.com/bknd@latest/dist/ui/index.js?compression=gzip&label=bknd/ui&t=1) ![gzipped size of bknd/ui](https://img.badgesize.io/https://unpkg.com/bknd@latest/dist/ui/index.js?compression=gzip&label=bknd/ui)
The size on npm is misleading, as the `bknd` package includes the backend, the ui components as well as the whole backend bundled into the cli including static assets. The size on npm is misleading, as the `bknd` package includes the backend, the ui components as well as the whole backend bundled into the cli including static assets.
@@ -52,7 +52,6 @@ Creating digital products always requires developing both the backend (the logic
* using a React framework (Next.js, React Router, Astro) * using a React framework (Next.js, React Router, Astro)
* 📦 Official API and React SDK with type-safety * 📦 Official API and React SDK with type-safety
* ⚛️ React elements for auto-configured authentication and media components * ⚛️ React elements for auto-configured authentication and media components
* 🛠️ MCP server, client and UI built-in to control your backend
## Structure ## Structure
The package is mainly split into 4 parts, each serving a specific purpose: The package is mainly split into 4 parts, each serving a specific purpose:
-2
View File
@@ -15,7 +15,6 @@ describe("AppServer", () => {
}, },
mcp: { mcp: {
enabled: false, enabled: false,
path: "/api/system/mcp",
}, },
}); });
} }
@@ -37,7 +36,6 @@ describe("AppServer", () => {
}, },
mcp: { mcp: {
enabled: false, enabled: false,
path: "/api/system/mcp",
}, },
}); });
} }
+1 -6
View File
@@ -61,11 +61,8 @@ function delayTypes() {
watcher_timeout = setTimeout(buildTypes, 1000); watcher_timeout = setTimeout(buildTypes, 1000);
} }
const dependencies = Object.keys(pkg.dependencies);
// collection of always-external packages // collection of always-external packages
const external = [ const external = [
...dependencies,
"bun:test", "bun:test",
"node:test", "node:test",
"node:assert/strict", "node:assert/strict",
@@ -89,10 +86,10 @@ async function buildApi() {
outDir: "dist", outDir: "dist",
external: [...external], external: [...external],
metafile: true, metafile: true,
target: "esnext",
platform: "browser", platform: "browser",
format: ["esm"], format: ["esm"],
splitting: false, splitting: false,
treeshake: true,
loader: { loader: {
".svg": "dataurl", ".svg": "dataurl",
}, },
@@ -248,8 +245,6 @@ async function buildAdapters() {
// base adapter handles // base adapter handles
tsup.build({ tsup.build({
...baseConfig(""), ...baseConfig(""),
target: "esnext",
platform: "neutral",
entry: ["src/adapter/index.ts"], entry: ["src/adapter/index.ts"],
outDir: "dist/adapter", outDir: "dist/adapter",
}), }),
+6 -6
View File
@@ -17,7 +17,7 @@ async function run(
}); });
// Read from stdout // Read from stdout
const reader = (proc.stdout as ReadableStream).getReader(); const reader = proc.stdout.getReader();
const decoder = new TextDecoder(); const decoder = new TextDecoder();
// Function to read chunks // Function to read chunks
@@ -30,7 +30,7 @@ async function run(
const text = decoder.decode(value); const text = decoder.decode(value);
if (!resolveCalled) { if (!resolveCalled) {
console.info(c.dim(text.replace(/\n$/, ""))); console.log(c.dim(text.replace(/\n$/, "")));
} }
onChunk( onChunk(
text, text,
@@ -189,21 +189,21 @@ const adapters = {
async function testAdapter(name: keyof typeof adapters) { async function testAdapter(name: keyof typeof adapters) {
const config = adapters[name]; const config = adapters[name];
console.info("adapter", c.cyan(name)); console.log("adapter", c.cyan(name));
await config.clean(); await config.clean();
const { proc, data } = await config.start(); const { proc, data } = await config.start();
console.info("proc:", proc.pid, "data:", c.cyan(data)); console.log("proc:", proc.pid, "data:", c.cyan(data));
//proc.kill();process.exit(0); //proc.kill();process.exit(0);
const add_env = "env" in config && config.env ? config.env : ""; const add_env = "env" in config && config.env ? config.env : "";
await $`TEST_URL=${data} TEST_ADAPTER=${name} ${add_env} bun run test:e2e`; await $`TEST_URL=${data} TEST_ADAPTER=${name} ${add_env} bun run test:e2e`;
console.info("DONE!"); console.log("DONE!");
while (!proc.killed) { while (!proc.killed) {
proc.kill("SIGINT"); proc.kill("SIGINT");
await Bun.sleep(250); await Bun.sleep(250);
console.info("Waiting for process to exit..."); console.log("Waiting for process to exit...");
} }
} }
-1
View File
@@ -7,7 +7,6 @@ async function generate() {
server: { server: {
mcp: { mcp: {
enabled: true, enabled: true,
path: "/mcp",
}, },
}, },
auth: { auth: {
+8 -8
View File
@@ -3,7 +3,7 @@
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,
"bin": "./dist/cli/index.js", "bin": "./dist/cli/index.js",
"version": "0.17.2", "version": "0.16.1",
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.", "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", "homepage": "https://bknd.io",
"repository": { "repository": {
@@ -15,7 +15,7 @@
}, },
"packageManager": "bun@1.2.19", "packageManager": "bun@1.2.19",
"engines": { "engines": {
"node": ">=22.13" "node": ">=22"
}, },
"scripts": { "scripts": {
"dev": "BKND_CLI_LOG_LEVEL=debug vite", "dev": "BKND_CLI_LOG_LEVEL=debug vite",
@@ -30,7 +30,7 @@
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly && tsc-alias", "build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly && tsc-alias",
"updater": "bun x npm-check-updates -ui", "updater": "bun x npm-check-updates -ui",
"cli": "LOCAL=1 bun src/cli/index.ts", "cli": "LOCAL=1 bun src/cli/index.ts",
"prepublishOnly": "bun run types && bun run test && bun run test:node && VITE_DB_URL=:memory: bun run test:e2e && bun run build:all && cp ../README.md ./", "prepublishOnly": "bun run types && bun run test && bun run test:node && bun run test:e2e && bun run build:all && cp ../README.md ./",
"postpublish": "rm -f README.md", "postpublish": "rm -f README.md",
"test": "ALL_TESTS=1 bun test --bail", "test": "ALL_TESTS=1 bun test --bail",
"test:all": "bun run test && bun run test:node", "test:all": "bun run test && bun run test:node",
@@ -41,9 +41,9 @@
"test:vitest:coverage": "vitest run --coverage", "test:vitest:coverage": "vitest run --coverage",
"test:e2e": "playwright test", "test:e2e": "playwright test",
"test:e2e:adapters": "bun run e2e/adapters.ts", "test:e2e:adapters": "bun run e2e/adapters.ts",
"test:e2e:ui": "VITE_DB_URL=:memory: playwright test --ui", "test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "VITE_DB_URL=:memory: playwright test --debug", "test:e2e:debug": "playwright test --debug",
"test:e2e:report": "VITE_DB_URL=:memory: playwright show-report", "test:e2e:report": "playwright show-report",
"docs:build-assets": "bun internal/docs.build-assets.ts" "docs:build-assets": "bun internal/docs.build-assets.ts"
}, },
"license": "FSL-1.1-MIT", "license": "FSL-1.1-MIT",
@@ -65,13 +65,12 @@
"hono": "4.8.3", "hono": "4.8.3",
"json-schema-library": "10.0.0-rc7", "json-schema-library": "10.0.0-rc7",
"json-schema-to-ts": "^3.1.1", "json-schema-to-ts": "^3.1.1",
"jsonv-ts": "0.8.2", "jsonv-ts": "^0.8.1",
"kysely": "0.27.6", "kysely": "0.27.6",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"oauth4webapi": "^2.11.1", "oauth4webapi": "^2.11.1",
"object-path-immutable": "^4.1.2", "object-path-immutable": "^4.1.2",
"radix-ui": "^1.1.3", "radix-ui": "^1.1.3",
"picocolors": "^1.1.1",
"swr": "^2.3.3" "swr": "^2.3.3"
}, },
"devDependencies": { "devDependencies": {
@@ -109,6 +108,7 @@
"libsql-stateless-easy": "^1.8.0", "libsql-stateless-easy": "^1.8.0",
"open": "^10.1.0", "open": "^10.1.0",
"openapi-types": "^12.1.3", "openapi-types": "^12.1.3",
"picocolors": "^1.1.1",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"postcss-preset-mantine": "^1.17.0", "postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1", "postcss-simple-vars": "^7.0.1",
+1 -1
View File
@@ -43,7 +43,7 @@ export type ApiOptions = {
} & ( } & (
| { | {
token?: string; token?: string;
user?: TApiUser | null; user?: TApiUser;
} }
| { | {
request: Request; request: Request;
+1 -2
View File
@@ -293,10 +293,9 @@ export class App<C extends Connection = Connection, Options extends AppOptions =
if (!this.mcp) { if (!this.mcp) {
throw new Error("MCP is not enabled"); throw new Error("MCP is not enabled");
} }
const mcpPath = this.modules.get("server").config.mcp.path;
return new McpClient({ return new McpClient({
url: "http://localhost" + mcpPath, url: "http://localhost/mcp",
fetch: this.server.request, fetch: this.server.request,
}); });
} }
+2 -2
View File
@@ -53,7 +53,7 @@ export function adapterTestSuite<
url: overrides.dbUrl ?? ":memory:", url: overrides.dbUrl ?? ":memory:",
origin: "localhost", origin: "localhost",
} as any, } as any,
{ force: false, id }, { id },
); );
expect(app).toBeDefined(); expect(app).toBeDefined();
expect(app.toJSON().server.cors.origin).toEqual("localhost"); expect(app.toJSON().server.cors.origin).toEqual("localhost");
@@ -69,7 +69,7 @@ export function adapterTestSuite<
}; };
test("responds with the same app id", async () => { test("responds with the same app id", async () => {
const fetcher = makeHandler(undefined, undefined, { force: false, id }); const fetcher = makeHandler(undefined, undefined, { id });
const { res, data } = await getConfig(fetcher); const { res, data } = await getConfig(fetcher);
expect(res.ok).toBe(true); expect(res.ok).toBe(true);
@@ -1,9 +1,10 @@
import { afterAll, beforeAll, describe, expect, it } from "bun:test"; import { afterAll, beforeAll, describe, expect, it } from "bun:test";
import { makeConfig, type CloudflareContext } from "./config"; import { makeApp } from "./modes/fresh";
import { makeConfig, type CfMakeConfigArgs } from "./config";
import { disableConsoleLog, enableConsoleLog } from "core/utils"; import { disableConsoleLog, enableConsoleLog } from "core/utils";
import { adapterTestSuite } from "adapter/adapter-test-suite"; import { adapterTestSuite } from "adapter/adapter-test-suite";
import { bunTestRunner } from "adapter/bun/test"; import { bunTestRunner } from "adapter/bun/test";
import { type CloudflareBkndConfig, createApp } from "./cloudflare-workers.adapter"; import type { CloudflareBkndConfig } from "./cloudflare-workers.adapter";
beforeAll(disableConsoleLog); beforeAll(disableConsoleLog);
afterAll(enableConsoleLog); afterAll(enableConsoleLog);
@@ -40,19 +41,18 @@ describe("cf adapter", () => {
expect(dynamicConfig.connection).toBeDefined(); expect(dynamicConfig.connection).toBeDefined();
}); });
adapterTestSuite<CloudflareBkndConfig, CloudflareContext<any>>(bunTestRunner, { adapterTestSuite<CloudflareBkndConfig, CfMakeConfigArgs<any>>(bunTestRunner, {
makeApp: async (c, a, o) => { makeApp: async (c, a, o) => {
return await createApp(c, { env: a } as any, o); return await makeApp(c, { env: a } as any, o);
}, },
makeHandler: (c, a, o) => { makeHandler: (c, a, o) => {
console.log("args", a);
return async (request: any) => { return async (request: any) => {
const app = await createApp( const app = await makeApp(
// needs a fallback, otherwise tries to launch D1 // needs a fallback, otherwise tries to launch D1
c ?? { c ?? {
connection: { url: DB_URL }, connection: { url: DB_URL },
}, },
a as any, a!,
o, o,
); );
return app.fetch(request); return app.fetch(request);
@@ -3,10 +3,10 @@
import type { RuntimeBkndConfig } from "bknd/adapter"; import type { RuntimeBkndConfig } from "bknd/adapter";
import { Hono } from "hono"; import { Hono } from "hono";
import { serveStatic } from "hono/cloudflare-workers"; import { serveStatic } from "hono/cloudflare-workers";
import type { MaybePromise } from "bknd"; import { getFresh } from "./modes/fresh";
import { getCached } from "./modes/cached";
import type { App, MaybePromise } from "bknd";
import { $console } from "bknd/utils"; import { $console } from "bknd/utils";
import { createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
import { registerAsyncsExecutionContext, makeConfig, type CloudflareContext } from "./config";
declare global { declare global {
namespace Cloudflare { namespace Cloudflare {
@@ -16,6 +16,7 @@ declare global {
export type CloudflareEnv = Cloudflare.Env; export type CloudflareEnv = Cloudflare.Env;
export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> & { export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> & {
mode?: "warm" | "fresh" | "cache";
bindings?: (args: Env) => MaybePromise<{ bindings?: (args: Env) => MaybePromise<{
kv?: KVNamespace; kv?: KVNamespace;
db?: D1Database; db?: D1Database;
@@ -33,31 +34,11 @@ export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> &
registerMedia?: boolean | ((env: Env) => void); registerMedia?: boolean | ((env: Env) => void);
}; };
export async function createApp<Env extends CloudflareEnv = CloudflareEnv>( export type Context<Env = CloudflareEnv> = {
config: CloudflareBkndConfig<Env>, request: Request;
ctx: Partial<CloudflareContext<Env>> = {}, env: Env;
opts: RuntimeOptions = { ctx: ExecutionContext;
// by default, require the app to be rebuilt every time };
force: true,
},
) {
const appConfig = await makeConfig(
{
...config,
onBuilt: async (app) => {
if (ctx.ctx) {
registerAsyncsExecutionContext(app, ctx?.ctx);
}
await config.onBuilt?.(app);
},
},
ctx,
);
return await createRuntimeApp<Env>(appConfig, ctx?.env, opts);
}
// compatiblity
export const getFresh = createApp;
export function serve<Env extends CloudflareEnv = CloudflareEnv>( export function serve<Env extends CloudflareEnv = CloudflareEnv>(
config: CloudflareBkndConfig<Env> = {}, config: CloudflareBkndConfig<Env> = {},
@@ -96,8 +77,23 @@ export function serve<Env extends CloudflareEnv = CloudflareEnv>(
} }
} }
const context = { request, env, ctx } as CloudflareContext<Env>; const context = { request, env, ctx } as Context<Env>;
const app = await createApp(config, context); const mode = config.mode ?? "warm";
let app: App;
switch (mode) {
case "fresh":
app = await getFresh(config, context, { force: true });
break;
case "warm":
app = await getFresh(config, context);
break;
case "cache":
app = await getCached(config, context);
break;
default:
throw new Error(`Unknown mode ${mode}`);
}
return app.fetch(request, env, ctx); return app.fetch(request, env, ctx);
}, },
+6 -6
View File
@@ -8,7 +8,7 @@ import { getBinding } from "./bindings";
import { d1Sqlite } from "./connection/D1Connection"; import { d1Sqlite } from "./connection/D1Connection";
import type { CloudflareBkndConfig, CloudflareEnv } from "."; import type { CloudflareBkndConfig, CloudflareEnv } from ".";
import { App } from "bknd"; import { App } from "bknd";
import type { Context as HonoContext, ExecutionContext } from "hono"; import type { Context, ExecutionContext } from "hono";
import { $console } from "bknd/utils"; import { $console } from "bknd/utils";
import { setCookie } from "hono/cookie"; import { setCookie } from "hono/cookie";
@@ -22,10 +22,10 @@ export const constants = {
}, },
}; };
export type CloudflareContext<Env extends CloudflareEnv = CloudflareEnv> = { export type CfMakeConfigArgs<Env extends CloudflareEnv = CloudflareEnv> = {
env: Env; env: Env;
ctx: ExecutionContext; ctx?: ExecutionContext;
request: Request; request?: Request;
}; };
function getCookieValue(cookies: string | null, name: string) { function getCookieValue(cookies: string | null, name: string) {
@@ -67,7 +67,7 @@ export function d1SessionHelper(config: CloudflareBkndConfig<any>) {
return undefined; return undefined;
}, },
set: (c: HonoContext, d1?: D1DatabaseSession) => { set: (c: Context, d1?: D1DatabaseSession) => {
if (!d1 || !config.d1?.session) return; if (!d1 || !config.d1?.session) return;
const session = d1.getBookmark(); const session = d1.getBookmark();
@@ -91,7 +91,7 @@ export function d1SessionHelper(config: CloudflareBkndConfig<any>) {
let media_registered: boolean = false; let media_registered: boolean = false;
export async function makeConfig<Env extends CloudflareEnv = CloudflareEnv>( export async function makeConfig<Env extends CloudflareEnv = CloudflareEnv>(
config: CloudflareBkndConfig<Env>, config: CloudflareBkndConfig<Env>,
args?: Partial<CloudflareContext<Env>>, args?: CfMakeConfigArgs<Env>,
) { ) {
if (!media_registered && config.registerMedia !== false) { if (!media_registered && config.registerMedia !== false) {
if (typeof config.registerMedia === "function") { if (typeof config.registerMedia === "function") {
+5 -9
View File
@@ -1,12 +1,8 @@
import { d1Sqlite, type D1ConnectionConfig } from "./connection/D1Connection"; import { d1Sqlite, type D1ConnectionConfig } from "./connection/D1Connection";
export { export * from "./cloudflare-workers.adapter";
getFresh, export { makeApp, getFresh } from "./modes/fresh";
createApp, export { getCached } from "./modes/cached";
serve,
type CloudflareEnv,
type CloudflareBkndConfig,
} from "./cloudflare-workers.adapter";
export { d1Sqlite, type D1ConnectionConfig }; export { d1Sqlite, type D1ConnectionConfig };
export { doSqlite, type DoConnectionConfig } from "./connection/DoConnection"; export { doSqlite, type DoConnectionConfig } from "./connection/DoConnection";
export { export {
@@ -16,10 +12,10 @@ export {
type GetBindingType, type GetBindingType,
type BindingMap, type BindingMap,
} from "./bindings"; } from "./bindings";
export { constants, type CloudflareContext } from "./config"; export { constants } from "./config";
export { StorageR2Adapter, registerMedia } from "./storage/StorageR2Adapter"; export { StorageR2Adapter, registerMedia } from "./storage/StorageR2Adapter";
export { registries } from "bknd"; export { registries } from "bknd";
export { devFsVitePlugin, devFsWrite } from "./vite"; export { devFsPlugin, devFsWrite } from "./vite";
// for compatibility with old code // for compatibility with old code
export function d1<DB extends D1Database | D1DatabaseSession = D1Database>( export function d1<DB extends D1Database | D1DatabaseSession = D1Database>(
@@ -0,0 +1,53 @@
import { App } from "bknd";
import { createRuntimeApp } from "bknd/adapter";
import type { CloudflareBkndConfig, Context, CloudflareEnv } from "../index";
import { makeConfig, registerAsyncsExecutionContext, constants } from "../config";
export async function getCached<Env extends CloudflareEnv = CloudflareEnv>(
config: CloudflareBkndConfig<Env>,
args: Context<Env>,
) {
const { env, ctx } = args;
const { kv } = await config.bindings?.(env)!;
if (!kv) throw new Error("kv namespace is not defined in cloudflare.bindings");
const key = config.key ?? "app";
const cachedConfig = await kv.get(key);
const initialConfig = cachedConfig ? JSON.parse(cachedConfig) : undefined;
async function saveConfig(__config: any) {
ctx.waitUntil(kv!.put(key, JSON.stringify(__config)));
}
const app = await createRuntimeApp(
{
...makeConfig(config, args),
initialConfig,
onBuilt: async (app) => {
registerAsyncsExecutionContext(app, ctx);
app.module.server.client.get(constants.cache_endpoint, async (c) => {
await kv.delete(key);
return c.json({ message: "Cache cleared" });
});
await config.onBuilt?.(app);
},
beforeBuild: async (app) => {
app.emgr.onEvent(
App.Events.AppConfigUpdatedEvent,
async ({ params: { app } }) => {
saveConfig(app.toJSON(true));
},
"sync",
);
await config.beforeBuild?.(app);
},
},
args,
);
if (!cachedConfig) {
saveConfig(app.toJSON(true));
}
return app;
}
+29
View File
@@ -0,0 +1,29 @@
import { createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
import type { CloudflareBkndConfig, Context, CloudflareEnv } from "../index";
import { makeConfig, registerAsyncsExecutionContext, type CfMakeConfigArgs } from "../config";
export async function makeApp<Env extends CloudflareEnv = CloudflareEnv>(
config: CloudflareBkndConfig<Env>,
args?: CfMakeConfigArgs<Env>,
opts?: RuntimeOptions,
) {
return await createRuntimeApp<Env>(await makeConfig(config, args), args?.env, opts);
}
export async function getFresh<Env extends CloudflareEnv = CloudflareEnv>(
config: CloudflareBkndConfig<Env>,
ctx: Context<Env>,
opts: RuntimeOptions = {},
) {
return await makeApp(
{
...config,
onBuilt: async (app) => {
registerAsyncsExecutionContext(app, ctx.ctx);
await config.onBuilt?.(app);
},
},
ctx,
opts,
);
}
+2 -2
View File
@@ -12,7 +12,7 @@ export function devFsVitePlugin({
}: { }: {
verbose?: boolean; verbose?: boolean;
configFile?: string; configFile?: string;
} = {}): any { }): Plugin {
let isDev = false; let isDev = false;
let projectRoot = ""; let projectRoot = "";
@@ -115,7 +115,7 @@ if (typeof globalThis !== 'undefined') {
return polyfill + code; return polyfill + code;
} }
}, },
} satisfies Plugin; };
} }
// Write function that uses the dev-fs polyfill injected by our Vite plugin // Write function that uses the dev-fs polyfill injected by our Vite plugin
+3 -9
View File
@@ -83,12 +83,8 @@ export async function createAdapterApp<Config extends BkndConfig = BkndConfig, A
} }
app = App.create(appConfig); app = App.create(appConfig);
apps.set(id, app);
if (!opts?.force) {
apps.set(id, app);
}
} }
return app; return app;
} }
@@ -173,9 +169,7 @@ export function serveStaticViaImport(opts?: { manifest?: Manifest }) {
return async (c: Context, next: Next) => { return async (c: Context, next: Next) => {
if (!files) { if (!files) {
const manifest = const manifest =
opts?.manifest || opts?.manifest || ((await import("bknd/dist/manifest.json")).default as Manifest);
((await import("bknd/dist/manifest.json", { with: { type: "json" } }))
.default as Manifest);
files = Object.values(manifest).flatMap((asset) => [asset.file, ...(asset.css || [])]); files = Object.values(manifest).flatMap((asset) => [asset.file, ...(asset.css || [])]);
} }
@@ -183,7 +177,7 @@ export function serveStaticViaImport(opts?: { manifest?: Manifest }) {
if (files.includes(path)) { if (files.includes(path)) {
try { try {
const content = await import(/* @vite-ignore */ `bknd/static/${path}?raw`, { const content = await import(/* @vite-ignore */ `bknd/static/${path}?raw`, {
with: { type: "text" }, assert: { type: "text" },
}).then((m) => m.default); }).then((m) => m.default);
if (content) { if (content) {
+1 -2
View File
@@ -221,7 +221,6 @@ export class AuthController extends Controller {
return user; return user;
}; };
const roles = Object.keys(this.auth.config.roles ?? {});
mcp.tool( mcp.tool(
// @todo: needs permission // @todo: needs permission
"auth_user_create", "auth_user_create",
@@ -232,7 +231,7 @@ export class AuthController extends Controller {
password: s.string({ minLength: 8 }), password: s.string({ minLength: 8 }),
role: s role: s
.string({ .string({
enum: roles.length > 0 ? roles : undefined, enum: Object.keys(this.auth.config.roles ?? {}),
}) })
.optional(), .optional(),
}), }),
-7
View File
@@ -473,10 +473,3 @@ export function deepFreeze<T extends object>(object: T): T {
return Object.freeze(object); return Object.freeze(object);
} }
export function convertNumberedObjectToArray(obj: object): any[] | object {
if (Object.keys(obj).every((key) => Number.isInteger(Number(key)))) {
return Object.values(obj);
}
return obj;
}
+3 -9
View File
@@ -1,16 +1,10 @@
import { v4, v7, validate, version as uuidVersion } from "uuid"; import { v4, v7 } from "uuid";
// generates v4 // generates v4
export function uuid(): string { export function uuid(): string {
return v4(); return v4();
} }
// generates v7
export function uuidv7(): string { export function uuidv7(): string {
return v7(); return v7();
}
// validate uuid
export function uuidValidate(uuid: string, version: 4 | 7): boolean {
return validate(uuid) && uuidVersion(uuid) === version;
} }
+15 -18
View File
@@ -1,15 +1,7 @@
import type { Handler } from "hono/types";
import type { ModuleBuildContext } from "modules"; import type { ModuleBuildContext } from "modules";
import { Controller } from "modules/Controller"; import { Controller } from "modules/Controller";
import { import { jsc, s, describeRoute, schemaToSpec, omitKeys, pickKeys, mcpTool } from "bknd/utils";
jsc,
s,
describeRoute,
schemaToSpec,
omitKeys,
pickKeys,
mcpTool,
convertNumberedObjectToArray,
} from "bknd/utils";
import * as SystemPermissions from "modules/permissions"; import * as SystemPermissions from "modules/permissions";
import type { AppDataConfig } from "../data-schema"; import type { AppDataConfig } from "../data-schema";
import type { EntityManager, EntityData } from "data/entities"; import type { EntityManager, EntityData } from "data/entities";
@@ -45,6 +37,14 @@ export class DataController extends Controller {
const hono = this.create().use(auth(), permission(SystemPermissions.accessApi)); const hono = this.create().use(auth(), permission(SystemPermissions.accessApi));
const entitiesEnum = this.getEntitiesEnum(this.em); const entitiesEnum = this.getEntitiesEnum(this.em);
// @todo: sample implementation how to augment handler with additional info
function handler<HH extends Handler>(name: string, h: HH): any {
const func = h;
// @ts-ignore
func.description = name;
return func;
}
// info // info
hono.get( hono.get(
"/", "/",
@@ -52,7 +52,10 @@ export class DataController extends Controller {
summary: "Retrieve data configuration", summary: "Retrieve data configuration",
tags: ["data"], tags: ["data"],
}), }),
(c) => c.json(this.em.toJSON()), handler("data info", (c) => {
// sample implementation
return c.json(this.em.toJSON());
}),
); );
// sync endpoint // sync endpoint
@@ -429,13 +432,7 @@ export class DataController extends Controller {
if (!this.entityExists(entity)) { if (!this.entityExists(entity)) {
return this.notFound(c); return this.notFound(c);
} }
const body = (await c.req.json()) as EntityData | EntityData[];
const _body = (await c.req.json()) as EntityData | EntityData[];
// @todo: check on jsonv-ts how to handle this better
// temporary fix for numbered object to array
// this happens when the MCP tool uses the allOf function
// to transform all validation targets into a single object
const body = convertNumberedObjectToArray(_body);
if (Array.isArray(body)) { if (Array.isArray(body)) {
const result = await this.em.mutator(entity).insertMany(body); const result = await this.em.mutator(entity).insertMany(body);
-3
View File
@@ -258,9 +258,6 @@ export class EntityManager<TBD extends object = DefaultDB> {
// @todo: centralize and add tests // @todo: centralize and add tests
hydrate(entity_name: string, _data: EntityData[]) { hydrate(entity_name: string, _data: EntityData[]) {
if (!Array.isArray(_data) || _data.length === 0) {
return [];
}
const entity = this.entity(entity_name); const entity = this.entity(entity_name);
const data: EntityData[] = []; const data: EntityData[] = [];
+1 -1
View File
@@ -51,7 +51,7 @@ export class Controller {
protected getEntitiesEnum(em: EntityManager<any>): s.StringSchema { protected getEntitiesEnum(em: EntityManager<any>): s.StringSchema {
const entities = em.entities.map((e) => e.name); const entities = em.entities.map((e) => e.name);
return entities.length > 0 ? s.anyOf([s.string({ enum: entities }), s.string()]) : s.string(); return entities.length > 0 ? s.string({ enum: entities }) : s.string();
} }
registerMcp(): void {} registerMcp(): void {}
+10 -2
View File
@@ -1,5 +1,13 @@
import { mark, stripMark, $console, s, objectEach, transformObject, McpServer } from "bknd/utils"; import {
import { DebugLogger } from "core/utils/DebugLogger"; mark,
stripMark,
$console,
s,
objectEach,
transformObject,
McpServer,
DebugLogger,
} from "bknd/utils";
import { Guard } from "auth/authorize/Guard"; import { Guard } from "auth/authorize/Guard";
import { env } from "core/env"; import { env } from "core/env";
import { BkndError } from "core/errors"; import { BkndError } from "core/errors";
+5 -9
View File
@@ -11,7 +11,6 @@ import { css, Style } from "hono/css";
import { Controller } from "modules/Controller"; import { Controller } from "modules/Controller";
import * as SystemPermissions from "modules/permissions"; import * as SystemPermissions from "modules/permissions";
import type { TApiUser } from "Api"; import type { TApiUser } from "Api";
import type { Manifest } from "vite";
const htmlBkndContextReplace = "<!-- BKND_CONTEXT -->"; const htmlBkndContextReplace = "<!-- BKND_CONTEXT -->";
@@ -33,7 +32,6 @@ export type AdminControllerOptions = {
debugRerenders?: boolean; debugRerenders?: boolean;
theme?: "dark" | "light" | "system"; theme?: "dark" | "light" | "system";
logoReturnPath?: string; logoReturnPath?: string;
manifest?: Manifest;
}; };
export class AdminController extends Controller { export class AdminController extends Controller {
@@ -196,10 +194,8 @@ export class AdminController extends Controller {
}; };
if (isProd) { if (isProd) {
let manifest: Manifest; let manifest: any;
if (this.options.manifest) { if (this.options.assetsPath.startsWith("http")) {
manifest = this.options.manifest;
} else if (this.options.assetsPath.startsWith("http")) {
manifest = await fetch(this.options.assetsPath + ".vite/manifest.json", { manifest = await fetch(this.options.assetsPath + ".vite/manifest.json", {
headers: { headers: {
Accept: "application/json", Accept: "application/json",
@@ -208,14 +204,14 @@ export class AdminController extends Controller {
} else { } else {
// @ts-ignore // @ts-ignore
manifest = await import("bknd/dist/manifest.json", { manifest = await import("bknd/dist/manifest.json", {
with: { type: "json" }, assert: { type: "json" },
}).then((res) => res.default); }).then((res) => res.default);
} }
try { try {
// @todo: load all marked as entry (incl. css) // @todo: load all marked as entry (incl. css)
assets.js = manifest["src/ui/main.tsx"]?.file!; assets.js = manifest["src/ui/main.tsx"].file;
assets.css = manifest["src/ui/main.tsx"]?.css?.[0] as any; assets.css = manifest["src/ui/main.tsx"].css[0] as any;
} catch (e) { } catch (e) {
$console.warn("Couldn't find assets in manifest", e); $console.warn("Couldn't find assets in manifest", e);
} }
-1
View File
@@ -24,7 +24,6 @@ export const serverConfigSchema = $object(
}), }),
mcp: s.strictObject({ mcp: s.strictObject({
enabled: s.boolean({ default: false }), enabled: s.boolean({ default: false }),
path: s.string({ default: "/api/system/mcp" }),
}), }),
}, },
{ {
+3 -5
View File
@@ -60,9 +60,8 @@ export class SystemController extends Controller {
register(app: App) { register(app: App) {
app.server.route("/api/system", this.getController()); app.server.route("/api/system", this.getController());
const config = app.modules.get("server").config;
if (!config.mcp.enabled) { if (!this.app.modules.get("server").config.mcp.enabled) {
return; return;
} }
@@ -85,7 +84,7 @@ export class SystemController extends Controller {
const level = consoleMap[message.params.level]; const level = consoleMap[message.params.level];
if (!level) return; if (!level) return;
$console[level]("MCP notification", message.params.message ?? message.params); $console[level](message.params.message);
} }
}); });
@@ -98,7 +97,7 @@ export class SystemController extends Controller {
explainEndpoint: true, explainEndpoint: true,
}, },
endpoint: { endpoint: {
path: config.mcp.path as any, path: "/mcp",
// @ts-ignore // @ts-ignore
_init: isNode() ? { duplex: "half" } : {}, _init: isNode() ? { duplex: "half" } : {},
}, },
@@ -376,7 +375,6 @@ export class SystemController extends Controller {
}), }),
(c) => (c) =>
c.json({ c.json({
id: this.app._id,
version: { version: {
config: c.get("app")?.version(), config: c.get("app")?.version(),
bknd: getVersion(), bknd: getVersion(),
@@ -16,7 +16,7 @@ const schema = s.partialObject({
metadata: s.string({ enum: ["copyright", "keep", "none"] }), metadata: s.string({ enum: ["copyright", "keep", "none"] }),
quality: s.number({ minimum: 1, maximum: 100 }), quality: s.number({ minimum: 1, maximum: 100 }),
}); });
export type CloudflareImageOptimizationSchema = s.Static<typeof schema>; type ImageOptimizationSchema = s.Static<typeof schema>;
export type CloudflareImageOptimizationOptions = { export type CloudflareImageOptimizationOptions = {
/** /**
@@ -38,12 +38,12 @@ export type CloudflareImageOptimizationOptions = {
* The default options to use * The default options to use
* @default {} * @default {}
*/ */
defaultOptions?: CloudflareImageOptimizationSchema; defaultOptions?: ImageOptimizationSchema;
/** /**
* The fixed options to use * The fixed options to use
* @default {} * @default {}
*/ */
fixedOptions?: CloudflareImageOptimizationSchema; fixedOptions?: ImageOptimizationSchema;
/** /**
* The cache control to use * The cache control to use
* @default public, max-age=31536000, immutable * @default public, max-age=31536000, immutable
-1
View File
@@ -1,6 +1,5 @@
export { export {
cloudflareImageOptimization, cloudflareImageOptimization,
type CloudflareImageOptimizationSchema,
type CloudflareImageOptimizationOptions, type CloudflareImageOptimizationOptions,
} from "./cloudflare/image-optimization.plugin"; } from "./cloudflare/image-optimization.plugin";
export { showRoutes, type ShowRoutesOptions } from "./dev/show-routes.plugin"; export { showRoutes, type ShowRoutesOptions } from "./dev/show-routes.plugin";
+2 -3
View File
@@ -20,9 +20,8 @@ const style = {
basicChildStyle: "pl-5 ml-1 border-l border-muted hover:border-primary/20", basicChildStyle: "pl-5 ml-1 border-l border-muted hover:border-primary/20",
container: "ml-[-10px]", container: "ml-[-10px]",
label: "text-primary/90 font-bold font-mono mr-2", label: "text-primary/90 font-bold font-mono mr-2",
stringValue: stringValue: "text-emerald-600 dark:text-emerald-500 font-mono select-text",
"text-emerald-600 dark:text-emerald-500 font-mono select-text text-wrap whitespace-wrap break-words", numberValue: "text-sky-500 dark:text-sky-400 font-mono",
numberValue: "text-sky-500 dark:text-sky-400 font-mono select-text",
nullValue: "text-zinc-400 font-mono", nullValue: "text-zinc-400 font-mono",
undefinedValue: "text-zinc-400 font-mono", undefinedValue: "text-zinc-400 font-mono",
otherValue: "text-zinc-400 font-mono", otherValue: "text-zinc-400 font-mono",
-8
View File
@@ -13,14 +13,6 @@ const Warning = ({ className, ...props }: IconProps) => (
/> />
); );
const Err = ({ className, ...props }: IconProps) => (
<TbAlertCircle
{...props}
className={twMerge("dark:text-red-300 text-red-700 cursor-help", className)}
/>
);
export const Icon = { export const Icon = {
Warning, Warning,
Err,
}; };
+6 -23
View File
@@ -1,6 +1,6 @@
import { useClickOutside, useHotkeys } from "@mantine/hooks"; import { useClickOutside, useHotkeys } from "@mantine/hooks";
import { IconChevronDown, IconChevronUp } from "@tabler/icons-react"; import { IconChevronDown, IconChevronUp } from "@tabler/icons-react";
import { transformObject, clampNumber } from "bknd/utils"; import { clampNumber } from "core/utils/numbers";
import { throttle } from "lodash-es"; import { throttle } from "lodash-es";
import { ScrollArea } from "radix-ui"; import { ScrollArea } from "radix-ui";
import { import {
@@ -19,21 +19,9 @@ import { appShellStore } from "ui/store";
import { useLocation } from "wouter"; import { useLocation } from "wouter";
export function Root({ children }: { children: React.ReactNode }) { export function Root({ children }: { children: React.ReactNode }) {
const sidebarWidths = appShellStore((store) => store.sidebars);
const style = transformObject(sidebarWidths, (value) => value.width);
return ( return (
<AppShellProvider> <AppShellProvider>
<div <div id="app-shell" data-shell="root" className="flex flex-1 flex-col select-none h-dvh">
id="app-shell"
data-shell="root"
className="flex flex-1 flex-col select-none h-dvh"
style={Object.fromEntries(
Object.entries(style).map(([key, value]) => [
`--sidebar-width-${key}`,
`${value}px`,
]),
)}
>
{children} {children}
</div> </div>
</AppShellProvider> </AppShellProvider>
@@ -397,16 +385,11 @@ export function MaxHeightContainer(props: ComponentPropsWithoutRef<"div">) {
} }
} }
useEffect(() => { useEffect(updateHeaderHeight, []);
updateHeaderHeight();
const resize = throttle(updateHeaderHeight, 500);
window.addEventListener("resize", resize); if (typeof window !== "undefined") {
window.addEventListener("resize", throttle(updateHeaderHeight, 500));
return () => { }
window.removeEventListener("resize", resize);
};
}, []);
return ( return (
<div ref={scrollRef} style={{ height: `${height - offset}px` }} {...props}> <div ref={scrollRef} style={{ height: `${height - offset}px` }} {...props}>
+1 -5
View File
@@ -74,7 +74,7 @@
@apply bg-background text-primary overflow-hidden h-dvh w-dvw; @apply bg-background text-primary overflow-hidden h-dvh w-dvw;
::selection { ::selection {
@apply bg-primary/15; @apply bg-muted;
} }
input { input {
@@ -122,7 +122,3 @@ body,
@utility debug-blue { @utility debug-blue {
@apply border-blue-500 border; @apply border-blue-500 border;
} }
@utility w-min-content {
width: min-content;
width: -webkit-min-content;
}
+1 -1
View File
@@ -1,7 +1,7 @@
import * as React from "react"; import * as React from "react";
import * as ReactDOM from "react-dom/client"; import * as ReactDOM from "react-dom/client";
import Admin from "./Admin"; import Admin from "./Admin";
//import "./main.css"; import "./main.css";
import "./styles.css"; import "./styles.css";
function render() { function render() {
+2 -2
View File
@@ -139,7 +139,7 @@ function Adapters() {
<span className="font-bold">Media Adapter:</span> <span className="font-bold">Media Adapter:</span>
{ctx.selected === null && <span className="opacity-70"> (Choose one)</span>} {ctx.selected === null && <span className="opacity-70"> (Choose one)</span>}
</Formy.Label> </Formy.Label>
<div className="grid grid-cols-2 md:flex flex-row gap-1 mb-2 flex-wrap"> <div className="flex flex-row gap-1 mb-2">
{ctx.schemas?.map((schema: any, i) => ( {ctx.schemas?.map((schema: any, i) => (
<Button <Button
key={i} key={i}
@@ -165,7 +165,7 @@ function Adapters() {
</div> </div>
{ctx.selected !== null && ( {ctx.selected !== null && (
<Formy.Group as="fieldset" error={ctx.errors.length > 0}> <Formy.Group as="fieldset" error={ctx.errors.length > 0}>
<Formy.Label as="legend" className="font-mono px-2 w-min-content"> <Formy.Label as="legend" className="font-mono px-2">
{autoFormatString(ctx.selectedSchema!.title!)} {autoFormatString(ctx.selectedSchema!.title!)}
</Formy.Label> </Formy.Label>
<FormContextOverride schema={ctx.selectedSchema} prefix={ctx.path}> <FormContextOverride schema={ctx.selectedSchema} prefix={ctx.path}>
@@ -1,16 +0,0 @@
import { McpClient, type McpClientConfig } from "jsonv-ts/mcp";
import { useBknd } from "ui/client/bknd";
const clients = new Map<string, McpClient>();
export function getClient(opts: McpClientConfig) {
if (!clients.has(JSON.stringify(opts))) {
clients.set(JSON.stringify(opts), new McpClient(opts));
}
return clients.get(JSON.stringify(opts))!;
}
export function useMcpClient() {
const { config } = useBknd();
return getClient({ url: window.location.origin + config.server.mcp.path });
}
+1 -2
View File
@@ -17,7 +17,6 @@ export default function ToolsMcp() {
const setFeature = useMcpStore((state) => state.setFeature); const setFeature = useMcpStore((state) => state.setFeature);
const content = useMcpStore((state) => state.content); const content = useMcpStore((state) => state.content);
const openSidebar = appShellStore((store) => store.toggleSidebar("default")); const openSidebar = appShellStore((store) => store.toggleSidebar("default"));
const mcpPath = config.server.mcp.path;
if (!config.server.mcp.enabled) { if (!config.server.mcp.enabled) {
return ( return (
@@ -40,7 +39,7 @@ export default function ToolsMcp() {
<TbWorld /> <TbWorld />
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<span className="block truncate text-sm font-mono leading-none"> <span className="block truncate text-sm font-mono leading-none">
{window.location.origin + mcpPath} {window.location.origin + "/mcp"}
</span> </span>
</div> </div>
</div> </div>
+8 -24
View File
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useRef, useState, useTransition } from "react"; import { useCallback, useEffect, useRef, useState, useTransition } from "react";
import { getTemplate } from "./utils"; import { getClient, getTemplate } from "./utils";
import { useMcpStore } from "./state"; import { useMcpStore } from "./state";
import { AppShell } from "ui/layouts/AppShell"; import { AppShell } from "ui/layouts/AppShell";
import { TbHistory, TbHistoryOff, TbRefresh } from "react-icons/tb"; import { TbHistory, TbHistoryOff, TbRefresh } from "react-icons/tb";
@@ -10,11 +10,9 @@ import { Field, Form } from "ui/components/form/json-schema-form";
import { Button } from "ui/components/buttons/Button"; import { Button } from "ui/components/buttons/Button";
import * as Formy from "ui/components/form/Formy"; import * as Formy from "ui/components/form/Formy";
import { appShellStore } from "ui/store"; import { appShellStore } from "ui/store";
import { Icon } from "ui/components/display/Icon";
import { useMcpClient } from "./hooks/use-mcp-client";
export function Sidebar({ open, toggle }) { export function Sidebar({ open, toggle }) {
const client = useMcpClient(); const client = getClient();
const closeSidebar = appShellStore((store) => store.closeSidebar("default")); const closeSidebar = appShellStore((store) => store.closeSidebar("default"));
const tools = useMcpStore((state) => state.tools); const tools = useMcpStore((state) => state.tools);
const setTools = useMcpStore((state) => state.setTools); const setTools = useMcpStore((state) => state.setTools);
@@ -22,18 +20,11 @@ export function Sidebar({ open, toggle }) {
const content = useMcpStore((state) => state.content); const content = useMcpStore((state) => state.content);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [query, setQuery] = useState<string>(""); const [query, setQuery] = useState<string>("");
const [error, setError] = useState<string | null>(null);
const handleRefresh = useCallback(async () => { const handleRefresh = useCallback(async () => {
setLoading(true); setLoading(true);
setError(null); const res = await client.listTools();
try { if (res) setTools(res.tools);
const res = await client.listTools();
if (res) setTools(res.tools);
} catch (e) {
console.error(e);
setError(String(e));
}
setLoading(false); setLoading(false);
}, []); }, []);
@@ -48,7 +39,6 @@ export function Sidebar({ open, toggle }) {
toggle={toggle} toggle={toggle}
renderHeaderRight={() => ( renderHeaderRight={() => (
<div className="flex flex-row gap-2 items-center"> <div className="flex flex-row gap-2 items-center">
{error && <Icon.Err title={error} className="size-5 pointer-events-auto" />}
<span className="flex-inline bg-primary/10 px-2 py-1.5 rounded-xl text-sm font-mono leading-none"> <span className="flex-inline bg-primary/10 px-2 py-1.5 rounded-xl text-sm font-mono leading-none">
{tools.length} {tools.length}
</span> </span>
@@ -98,7 +88,7 @@ export function Content() {
const [result, setResult] = useState<object | null>(null); const [result, setResult] = useState<object | null>(null);
const historyVisible = useMcpStore((state) => state.historyVisible); const historyVisible = useMcpStore((state) => state.historyVisible);
const setHistoryVisible = useMcpStore((state) => state.setHistoryVisible); const setHistoryVisible = useMcpStore((state) => state.setHistoryVisible);
const client = useMcpClient(); const client = getClient();
const jsonViewerTabsRef = useRef<JsonViewerTabsRef>(null); const jsonViewerTabsRef = useRef<JsonViewerTabsRef>(null);
const hasInputSchema = const hasInputSchema =
content?.inputSchema && Object.keys(content.inputSchema.properties ?? {}).length > 0; content?.inputSchema && Object.keys(content.inputSchema.properties ?? {}).length > 0;
@@ -139,7 +129,7 @@ export function Content() {
return ( return (
<Form <Form
className="flex flex-grow flex-col min-w-0 max-w-screen" className="flex flex-grow flex-col max-w-screen"
key={content.name} key={content.name}
schema={{ schema={{
title: "InputSchema", title: "InputSchema",
@@ -180,12 +170,7 @@ export function Content() {
</AppShell.SectionHeaderTitle> </AppShell.SectionHeaderTitle>
</AppShell.SectionHeader> </AppShell.SectionHeader>
<div className="flex flex-grow flex-row w-vw"> <div className="flex flex-grow flex-row w-vw">
<div <div className="flex flex-grow flex-col w-full">
className="flex flex-grow flex-col max-w-full"
style={{
width: "calc(100% - var(--sidebar-width-right) - 1px)",
}}
>
<AppShell.Scrollable> <AppShell.Scrollable>
<div key={JSON.stringify(content)} className="flex flex-col py-4 px-5 gap-4"> <div key={JSON.stringify(content)} className="flex flex-col py-4 px-5 gap-4">
<p className="text-primary/80">{content?.description}</p> <p className="text-primary/80">{content?.description}</p>
@@ -234,9 +219,8 @@ const History = () => {
<JsonViewer <JsonViewer
key={`${item.type}-${i}`} key={`${item.type}-${i}`}
json={item.data} json={item.data}
title={item.type.substring(0, 3)} title={item.type}
expand={2} expand={2}
showCopy
/> />
))} ))}
</div> </div>
+12
View File
@@ -1,5 +1,17 @@
import { McpClient, type McpClientConfig } from "jsonv-ts/mcp";
import { Draft2019 } from "json-schema-library"; import { Draft2019 } from "json-schema-library";
const clients = new Map<string, McpClient>();
export function getClient(
{ url, ...opts }: McpClientConfig = { url: window.location.origin + "/mcp" },
) {
if (!clients.has(String(url))) {
clients.set(String(url), new McpClient({ url, ...opts }));
}
return clients.get(String(url))!;
}
export function getTemplate(schema: object) { export function getTemplate(schema: object) {
if (!schema || schema === undefined || schema === null) return undefined; if (!schema || schema === undefined || schema === null) return undefined;
+2 -4
View File
@@ -8,10 +8,8 @@ html.fixed,
html.fixed body { html.fixed body {
top: 0; top: 0;
left: 0; left: 0;
right: 0; height: 100%;
bottom: 0; width: 100%;
height: 100dvh;
width: 100dvw;
position: fixed; position: fixed;
overflow: hidden; overflow: hidden;
overscroll-behavior-x: contain; overscroll-behavior-x: contain;
-3
View File
@@ -9,9 +9,6 @@ import { nodeSqlite } from "./src/adapter/node/connection/NodeSqliteConnection";
import { libsql } from "./src/data/connection/sqlite/libsql/LibsqlConnection"; import { libsql } from "./src/data/connection/sqlite/libsql/LibsqlConnection";
import { $console } from "core/utils/console"; import { $console } from "core/utils/console";
import { createClient } from "@libsql/client"; import { createClient } from "@libsql/client";
import util from "node:util";
util.inspect.defaultOptions.depth = 5;
registries.media.register("local", StorageLocalAdapter); registries.media.register("local", StorageLocalAdapter);
+12 -5
View File
@@ -15,7 +15,7 @@
}, },
"app": { "app": {
"name": "bknd", "name": "bknd",
"version": "0.17.1", "version": "0.16.1",
"bin": "./dist/cli/index.js", "bin": "./dist/cli/index.js",
"dependencies": { "dependencies": {
"@cfworker/json-schema": "^4.1.1", "@cfworker/json-schema": "^4.1.1",
@@ -35,7 +35,7 @@
"hono": "4.8.3", "hono": "4.8.3",
"json-schema-library": "10.0.0-rc7", "json-schema-library": "10.0.0-rc7",
"json-schema-to-ts": "^3.1.1", "json-schema-to-ts": "^3.1.1",
"jsonv-ts": "0.8.2", "jsonv-ts": "^0.8.1",
"kysely": "0.27.6", "kysely": "0.27.6",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"oauth4webapi": "^2.11.1", "oauth4webapi": "^2.11.1",
@@ -151,6 +151,7 @@
"bknd": "workspace:*", "bknd": "workspace:*",
"kysely-neon": "^1.3.0", "kysely-neon": "^1.3.0",
"tsup": "^8.4.0", "tsup": "^8.4.0",
"typescript": "^5.8.2",
}, },
"optionalDependencies": { "optionalDependencies": {
"kysely": "^0.27.6", "kysely": "^0.27.6",
@@ -1231,7 +1232,7 @@
"@types/babel__traverse": ["@types/babel__traverse@7.20.6", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg=="], "@types/babel__traverse": ["@types/babel__traverse@7.20.6", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg=="],
"@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="], "@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="],
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
@@ -2515,7 +2516,7 @@
"jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="], "jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="],
"jsonv-ts": ["jsonv-ts@0.8.2", "", { "optionalDependencies": { "hono": "*" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-1Z7+maCfoGGqBPu5vN8rU9gIsW7OatYmn+STBTPkybbtNqeMzAoJDDrXHjsZ89x5dPH9W+OgMpNLtN0ouwiMYg=="], "jsonv-ts": ["jsonv-ts@0.8.1", "", { "optionalDependencies": { "hono": "*" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-kqt1OHZ4WM92PDAxySZeGGzccZr6q5YdKpM8c7QWkwGoaa1azwTG5lV9SN3PT4kVgI0OYFDr3OGkgCszLQ+WPw=="],
"jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="], "jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="],
@@ -3831,6 +3832,10 @@
"@bknd/plasmic/typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], "@bknd/plasmic/typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"@bknd/postgres/@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="],
"@bknd/postgres/typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"@bknd/sqlocal/typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], "@bknd/sqlocal/typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"@bundled-es-modules/cookie/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], "@bundled-es-modules/cookie/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
@@ -4073,7 +4078,7 @@
"@testing-library/jest-dom/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], "@testing-library/jest-dom/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="],
"@types/bun/bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="], "@types/bun/bun-types": ["bun-types@1.2.20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="],
"@typescript-eslint/experimental-utils/eslint-utils": ["eslint-utils@2.1.0", "", { "dependencies": { "eslint-visitor-keys": "^1.1.0" } }, "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg=="], "@typescript-eslint/experimental-utils/eslint-utils": ["eslint-utils@2.1.0", "", { "dependencies": { "eslint-visitor-keys": "^1.1.0" } }, "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg=="],
@@ -4679,6 +4684,8 @@
"@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.3", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg=="], "@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.3", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg=="],
"@bknd/postgres/@types/bun/bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],
"@bundled-es-modules/tough-cookie/tough-cookie/universalify": ["universalify@0.2.0", "", {}, "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="], "@bundled-es-modules/tough-cookie/tough-cookie/universalify": ["universalify@0.2.0", "", {}, "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="],
"@cloudflare/vitest-pool-workers/miniflare/sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], "@cloudflare/vitest-pool-workers/miniflare/sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="],
+3 -3
View File
@@ -5,7 +5,7 @@ WORKDIR /app
# define bknd version to be used as: # define bknd version to be used as:
# `docker build --build-arg VERSION=<version> -t bknd .` # `docker build --build-arg VERSION=<version> -t bknd .`
ARG VERSION=0.17.1 ARG VERSION=0.13.0
# Install & copy required cli # Install & copy required cli
RUN npm install --omit=dev bknd@${VERSION} RUN npm install --omit=dev bknd@${VERSION}
@@ -16,10 +16,10 @@ FROM node:24-alpine
WORKDIR /app WORKDIR /app
# Install required dependencies # Install pm2 and libsql
RUN npm install -g pm2 RUN npm install -g pm2
RUN echo '{"type":"module"}' > package.json RUN echo '{"type":"module"}' > package.json
RUN npm install jsonv-ts @libsql/client RUN npm install @libsql/client
# Create volume and init args # Create volume and init args
VOLUME /data VOLUME /data
+67 -65
View File
@@ -3,77 +3,79 @@
import * as React from "react"; import * as React from "react";
export const examples = { export const examples = {
adminRich: { adminRich: {
path: "github/bknd-io/bknd-examples", path: "github/bknd-io/bknd-examples",
startScript: "example-admin-rich", startScript: "example-admin-rich",
initialPath: "/data/schema", initialPath: "/data/schema"
}, }
}; };
export const StackBlitz = ({ export const StackBlitz = ({
path, path,
ratio = 9 / 16, ratio = 9 / 16,
example, example,
...props ...props
}: { }: {
path?: string; path?: string;
ratio?: number; ratio?: number;
example?: keyof typeof examples; example?: keyof typeof examples;
[key: string]: unknown; [key: string]: unknown;
}) => { }) => {
const selected = example ? examples[example] : undefined; const selected = example ? examples[example] : undefined;
const finalPath = path || selected?.path || "github/bknd-io/bknd-examples"; const finalPath = path || selected?.path || "github/bknd-io/bknd-examples";
const params = new URLSearchParams({ const params = new URLSearchParams({
ctl: "1", ctl: "1",
hideExplorer: "1", hideExplorer: "1",
embed: "1", embed: "1",
view: "preview", view: "preview",
...(selected || {}), ...(selected || {}),
...props, ...props
}); });
const url = new URL(`https://stackblitz.com/${finalPath}?${params.toString()}`); const url = new URL(
`https://stackblitz.com/${finalPath}?${params.toString()}`
);
return ( return (
<> <>
<div <div
style={{ style={{
position: "relative", position: "relative",
overflow: "hidden", overflow: "hidden",
width: "100%", width: "100%",
paddingTop: `${ratio * 100}%`, paddingTop: `${ratio * 100}%`
}} }}
> >
<iframe <iframe
width="100%" width="100%"
height="100%" height="100%"
src={url.toString()} src={url.toString()}
style={{ style={{
position: "absolute", position: "absolute",
top: 0, top: 0,
left: 0, left: 0,
bottom: 0, bottom: 0,
right: 0, right: 0,
border: "none", border: "none"
}} }}
/> />
</div> </div>
<div <div
style={{ style={{
fontSize: "80%", fontSize: "80%",
opacity: 0.7, opacity: 0.7,
marginTop: "0.2rem", marginTop: "0.2rem",
marginBottom: "1rem", marginBottom: "1rem",
textAlign: "center", textAlign: "center"
}} }}
> >
If you&rsquo;re having issues viewing it inline,{" "} If you&rsquo;re having issues viewing it inline,{" "}
<a href={url.toString()} target="_blank" rel="noreferrer"> <a href={url.toString()} target="_blank" rel="noreferrer">
try in a new tab try in a new tab
</a> </a>
. .
</div> </div>
</> </>
); );
}; };
@@ -29,15 +29,15 @@ const app = createApp();
app.emgr.onEvent(AppEvents.AppRequest, async (event) => { app.emgr.onEvent(AppEvents.AppRequest, async (event) => {
// ^? AppRequest // ^? AppRequest
console.log("Request received", event.params.request.url); console.log("Request received", event.request.url);
}); });
app.emgr.on("app-request", async (event) => { app.emgr.on("app-request", async (event) => {
console.log("Request received", event.params.request.url); console.log("Request received", event.request.url);
}); });
app.emgr.onAny(async (event, name) => { app.emgr.onAny(async (event) => {
console.log("Event received", event, name); console.log("Event received", event.slug);
}); });
``` ```
@@ -49,7 +49,7 @@ import { AppEvents } from "bknd";
export default { export default {
onBuilt: (app) => { onBuilt: (app) => {
app.emgr.onEvent(AppEvents.AppRequest, async (event) => { app.emgr.onEvent(AppEvents.AppRequest, async (event) => {
console.log("Request received", event.params.request.url); console.log("Request received", event.request.url);
}); });
}, },
}; };
@@ -63,7 +63,7 @@ By default, listeners are registered as `async` listeners, meaning that the list
app.emgr.onEvent( app.emgr.onEvent(
AppEvents.AppRequest, AppEvents.AppRequest,
async (event) => { async (event) => {
console.log("Request received", event.params.request.url); console.log("Request received", event.request.url);
}, },
{ mode: "sync" }, { mode: "sync" },
); );
@@ -108,7 +108,7 @@ import { config } from "@/bknd";
import { serve } from "bknd/adapter/nextjs"; import { serve } from "bknd/adapter/nextjs";
// optionally, you can set the runtime to edge for better performance // optionally, you can set the runtime to edge for better performance
// export const runtime = "edge"; export const runtime = "edge";
const handler = serve({ const handler = serve({
...config, ...config,
@@ -42,7 +42,8 @@ bun add bknd
## Serve the API ## Serve the API
If you don't choose anything specific, it uses the first D1 binding it finds. If you don't choose anything specific, the following code will use the `warm` mode and uses the first D1 binding it finds. See the
chapter [Using a different mode](#using-a-different-mode) for available modes.
```ts title="src/index.ts" ```ts title="src/index.ts"
import { serve, d1 } from "bknd/adapter/cloudflare"; import { serve, d1 } from "bknd/adapter/cloudflare";
@@ -129,6 +130,46 @@ export default serve<Env>({
The property `app.server` is a [Hono](https://hono.dev/) instance, you can literally anything you can do with Hono. The property `app.server` is a [Hono](https://hono.dev/) instance, you can literally anything you can do with Hono.
## Using a different mode
With the Cloudflare Workers adapter, you're being offered to 4 modes to choose from (default:
`warm`):
| Mode | Description | Use Case |
| :-------- | :----------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- |
| `fresh` | On every request, the configuration gets refetched, app built and then served. | Ideal if you don't want to deal with eviction, KV or Durable Objects. |
| `warm` | It tries to keep the built app in memory for as long as possible, and rebuilds if evicted. | Better response times, should be the default choice. |
| `cache` | The configuration is fetched from KV to reduce the initial roundtrip to the database. | Generally faster response times with irregular access patterns. |
### Modes: `fresh` and `warm`
To use either `fresh` or `warm`, all you have to do is adding the desired mode to `cloudflare.
mode`, like so:
```ts
import { serve } from "bknd/adapter/cloudflare";
export default serve({
// ...
mode: "fresh", // mode: "fresh" | "warm" | "cache" | "durable"
});
```
### Mode: `cache`
For the cache mode to work, you also need to specify the KV to be used. For this, use the
`bindings` property:
```ts
import { serve } from "bknd/adapter/cloudflare";
export default serve<Env>({
// ...
mode: "cache",
bindings: ({ env }) => ({ kv: env.KV }),
});
```
## D1 Sessions (experimental) ## D1 Sessions (experimental)
D1 now supports to enable [global read replication](https://developers.cloudflare.com/d1/best-practices/read-replication/). This allows to reduce latency by reading from the closest region. In order for this to work, D1 has to be started from a bookmark. You can enable this behavior on bknd by setting the `d1.session` property: D1 now supports to enable [global read replication](https://developers.cloudflare.com/d1/best-practices/read-replication/). This allows to reduce latency by reading from the closest region. In order for this to work, D1 has to be started from a bookmark. You can enable this behavior on bknd by setting the `d1.session` property:
@@ -137,6 +178,9 @@ D1 now supports to enable [global read replication](https://developers.cloudflar
import { serve } from "bknd/adapter/cloudflare"; import { serve } from "bknd/adapter/cloudflare";
export default serve({ export default serve({
// currently recommended to use "fresh" mode
// otherwise consecutive requests will use the same bookmark
mode: "fresh",
// ... // ...
d1: { d1: {
// enables D1 sessions // enables D1 sessions
@@ -163,7 +207,7 @@ The [Cloudflare Vite Plugin](https://developers.cloudflare.com/workers/vite-plug
To fix this, bknd exports a Vite plugin that provides filesystem access during development. You can use it by adding the following to your `vite.config.ts` file: To fix this, bknd exports a Vite plugin that provides filesystem access during development. You can use it by adding the following to your `vite.config.ts` file:
```ts ```ts
import { devFsVitePlugin } from "bknd/adapter/cloudflare"; import { devFsVitePlugin } from "bknd/adapter/cloudflare/vite";
export default defineConfig({ export default defineConfig({
plugins: [devFsVitePlugin()], // [!code highlight] plugins: [devFsVitePlugin()], // [!code highlight]
@@ -173,7 +217,7 @@ export default defineConfig({
Now to use this polyfill, you can use the `devFsWrite` function to write files to the filesystem. Now to use this polyfill, you can use the `devFsWrite` function to write files to the filesystem.
```ts ```ts
import { devFsWrite } from "bknd/adapter/cloudflare"; // [!code highlight] import { devFsWrite } from "bknd/adapter/cloudflare/vite"; // [!code highlight]
import { syncTypes } from "bknd/plugins"; import { syncTypes } from "bknd/plugins";
export default { export default {
+1 -1
View File
@@ -19,7 +19,7 @@ Glad you're here! **bknd** is a lightweight, infrastructure agnostic and feature
Here is a preview of **bknd** in StackBlitz: Here is a preview of **bknd** in StackBlitz:
<StackBlitz path="github/bknd-io/bknd-demo" initialPath="/" /> <StackBlitz {...examples.adminRich} />
<Accordions> <Accordions>
<Accordion title="What's going on?"> <Accordion title="What's going on?">
@@ -224,7 +224,7 @@ Example using `@neondatabase/serverless`:
import { createCustomPostgresConnection } from "@bknd/postgres"; import { createCustomPostgresConnection } from "@bknd/postgres";
import { NeonDialect } from "kysely-neon"; import { NeonDialect } from "kysely-neon";
const neon = createCustomPostgresConnection("neon", NeonDialect); const neon = createCustomPostgresConnection(NeonDialect);
serve({ serve({
connection: neon({ connection: neon({
@@ -247,7 +247,7 @@ const xata = new client({
branch: process.env.XATA_BRANCH, branch: process.env.XATA_BRANCH,
}); });
const xataConnection = createCustomPostgresConnection("xata", XataDialect, { const xataConnection = createCustomPostgresConnection(XataDialect, {
supports: { supports: {
batching: false, batching: false,
}, },
@@ -24,7 +24,7 @@ bknd includes a fully featured MCP server that can be used to interact with the
<source src="/content/mcp/v0.17_mcp_o.mp4" type="video/mp4" /> <source src="/content/mcp/v0.17_mcp_o.mp4" type="video/mp4" />
</video> </video>
Once enabled, you can access the MCP UI at `/mcp`, or choose "MCP" from the top right user menu. Once enabled, you can access the MCP UI at `/mcp` or choose "MCP" from the top right user menu.
## Enable MCP ## Enable MCP
@@ -54,7 +54,7 @@ The implementation is closely following the [MCP spec 2025-06-18](https://modelc
import { McpClient } from "bknd/utils"; import { McpClient } from "bknd/utils";
const client = new McpClient({ const client = new McpClient({
url: "http://localhost:1337/api/system/mcp", url: "http://localhost:1337/mcp",
}); });
``` ```
@@ -128,7 +128,7 @@ Pasting the following config into your Cursor `~/.cursor/mcp.json` file is the r
{ {
"mcpServers": { "mcpServers": {
"bknd": { "bknd": {
"url": "http://localhost:1337/api/system/mcp" "url": "http://localhost:1337/mcp"
} }
} }
} }
@@ -155,7 +155,7 @@ Add this to your VS Code MCP config. See [VS Code MCP docs](https://code.visuals
"servers": { "servers": {
"bknd": { "bknd": {
"type": "http", "type": "http",
"url": "http://localhost:1337/api/system/mcp" "url": "http://localhost:1337/mcp"
} }
} }
} }
@@ -188,7 +188,7 @@ When using the Streamable HTTP transport, you can pass the `Authorization` heade
```typescript ```typescript
const client = new McpClient({ const client = new McpClient({
url: "http://localhost:1337/api/system/mcp", url: "http://localhost:1337/mcp",
headers: { headers: {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },
+42 -115
View File
@@ -35,7 +35,8 @@
"minLength": 8 "minLength": 8
}, },
"role": { "role": {
"type": "string" "type": "string",
"enum": []
} }
}, },
"required": [ "required": [
@@ -133,17 +134,10 @@
], ],
"properties": { "properties": {
"entity": { "entity": {
"anyOf": [ "type": "string",
{ "enum": [
"type": "string", "users",
"enum": [ "media"
"users",
"media"
]
},
{
"type": "string"
}
], ],
"$target": "param" "$target": "param"
}, },
@@ -167,17 +161,10 @@
], ],
"properties": { "properties": {
"entity": { "entity": {
"anyOf": [ "type": "string",
{ "enum": [
"type": "string", "users",
"enum": [ "media"
"users",
"media"
]
},
{
"type": "string"
}
], ],
"$target": "param" "$target": "param"
}, },
@@ -207,17 +194,10 @@
], ],
"properties": { "properties": {
"entity": { "entity": {
"anyOf": [ "type": "string",
{ "enum": [
"type": "string", "users",
"enum": [ "media"
"users",
"media"
]
},
{
"type": "string"
}
], ],
"$target": "param" "$target": "param"
}, },
@@ -255,17 +235,10 @@
], ],
"properties": { "properties": {
"entity": { "entity": {
"anyOf": [ "type": "string",
{ "enum": [
"type": "string", "users",
"enum": [ "media"
"users",
"media"
]
},
{
"type": "string"
}
], ],
"$target": "param" "$target": "param"
}, },
@@ -303,17 +276,10 @@
], ],
"properties": { "properties": {
"entity": { "entity": {
"anyOf": [ "type": "string",
{ "enum": [
"type": "string", "users",
"enum": [ "media"
"users",
"media"
]
},
{
"type": "string"
}
], ],
"$target": "param" "$target": "param"
} }
@@ -331,17 +297,10 @@
], ],
"properties": { "properties": {
"entity": { "entity": {
"anyOf": [ "type": "string",
{ "enum": [
"type": "string", "users",
"enum": [ "media"
"users",
"media"
]
},
{
"type": "string"
}
], ],
"$target": "param" "$target": "param"
}, },
@@ -375,17 +334,10 @@
], ],
"properties": { "properties": {
"entity": { "entity": {
"anyOf": [ "type": "string",
{ "enum": [
"type": "string", "users",
"enum": [ "media"
"users",
"media"
]
},
{
"type": "string"
}
], ],
"$target": "param" "$target": "param"
}, },
@@ -458,17 +410,10 @@
], ],
"properties": { "properties": {
"entity": { "entity": {
"anyOf": [ "type": "string",
{ "enum": [
"type": "string", "users",
"enum": [ "media"
"users",
"media"
]
},
{
"type": "string"
}
], ],
"$target": "param" "$target": "param"
}, },
@@ -517,17 +462,10 @@
], ],
"properties": { "properties": {
"entity": { "entity": {
"anyOf": [ "type": "string",
{ "enum": [
"type": "string", "users",
"enum": [ "media"
"users",
"media"
]
},
{
"type": "string"
}
], ],
"$target": "param" "$target": "param"
}, },
@@ -556,17 +494,10 @@
], ],
"properties": { "properties": {
"entity": { "entity": {
"anyOf": [ "type": "string",
{ "enum": [
"type": "string", "users",
"enum": [ "media"
"users",
"media"
]
},
{
"type": "string"
}
], ],
"$target": "param" "$target": "param"
}, },
@@ -4063,10 +3994,6 @@
"enabled": { "enabled": {
"type": "boolean", "type": "boolean",
"default": false "default": false
},
"path": {
"type": "string",
"default": "/api/system/mcp"
} }
} }
} }
+1 -1
View File
@@ -27,7 +27,7 @@ async function generateDocument({
resources: ReturnType<Resource["toJSON"]>[]; resources: ReturnType<Resource["toJSON"]>[];
}) { }) {
return `--- return `---
title: "Tools & Resources" title: "MCP"
description: "Tools & Resources of the built-in full featured MCP server." description: "Tools & Resources of the built-in full featured MCP server."
tags: ["documentation"] tags: ["documentation"]
--- ---
+1 -1
View File
@@ -5,7 +5,7 @@
* We're using separate files, so that "wrangler" doesn't get bundled with your worker. * We're using separate files, so that "wrangler" doesn't get bundled with your worker.
*/ */
import { withPlatformProxy } from "bknd/adapter/cloudflare/proxy"; import { withPlatformProxy } from "bknd/adapter/cloudflare";
import config from "./config.ts"; import config from "./config.ts";
export default withPlatformProxy(config); export default withPlatformProxy(config);
+14
View File
@@ -1,7 +1,21 @@
import type { CloudflareBkndConfig } from "bknd/adapter/cloudflare"; import type { CloudflareBkndConfig } from "bknd/adapter/cloudflare";
import { syncTypes } from "bknd/plugins";
import { writeFile } from "node:fs/promises";
const isDev = !import.meta.env.PROD;
export default { export default {
d1: { d1: {
session: true, session: true,
}, },
options: {
plugins: [
syncTypes({
enabled: isDev,
write: async (et) => {
await writeFile("bknd-types.d.ts", et.toString());
},
}),
],
},
} satisfies CloudflareBkndConfig; } satisfies CloudflareBkndConfig;
+1 -1
View File
@@ -2,7 +2,7 @@
"$schema": "node_modules/wrangler/config-schema.json", "$schema": "node_modules/wrangler/config-schema.json",
"name": "bknd-cf-worker-example", "name": "bknd-cf-worker-example",
"main": "src/index.ts", "main": "src/index.ts",
"compatibility_date": "2025-08-03", "compatibility_date": "2025-02-04",
"compatibility_flags": ["nodejs_compat"], "compatibility_flags": ["nodejs_compat"],
"workers_dev": true, "workers_dev": true,
"minify": true, "minify": true,
+3 -3
View File
@@ -59,7 +59,7 @@ You can create a custom kysely postgres dialect by using the `createCustomPostgr
```ts ```ts
import { createCustomPostgresConnection } from "@bknd/postgres"; import { createCustomPostgresConnection } from "@bknd/postgres";
const connection = createCustomPostgresConnection("my_postgres_dialect", MyDialect)({ const connection = createCustomPostgresConnection(MyDialect)({
// your custom dialect configuration // your custom dialect configuration
supports: { supports: {
batching: true batching: true
@@ -75,7 +75,7 @@ const connection = createCustomPostgresConnection("my_postgres_dialect", MyDiale
import { createCustomPostgresConnection } from "@bknd/postgres"; import { createCustomPostgresConnection } from "@bknd/postgres";
import { NeonDialect } from "kysely-neon"; import { NeonDialect } from "kysely-neon";
const connection = createCustomPostgresConnection("neon", NeonDialect)({ const connection = createCustomPostgresConnection(NeonDialect)({
connectionString: process.env.NEON, connectionString: process.env.NEON,
}); });
``` ```
@@ -94,7 +94,7 @@ const xata = new client({
branch: process.env.XATA_BRANCH, branch: process.env.XATA_BRANCH,
}); });
const connection = createCustomPostgresConnection("xata", XataDialect, { const connection = createCustomPostgresConnection(XataDialect, {
supports: { supports: {
batching: false, batching: false,
}, },
+3 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@bknd/postgres", "name": "@bknd/postgres",
"version": "0.2.0", "version": "0.1.0",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"module": "dist/index.js", "module": "dist/index.js",
@@ -31,7 +31,8 @@
"@xata.io/kysely": "^0.2.1", "@xata.io/kysely": "^0.2.1",
"bknd": "workspace:*", "bknd": "workspace:*",
"kysely-neon": "^1.3.0", "kysely-neon": "^1.3.0",
"tsup": "^8.4.0" "tsup": "^8.4.0",
"typescript": "^5.8.2"
}, },
"tsup": { "tsup": {
"entry": ["src/index.ts"], "entry": ["src/index.ts"],
@@ -1,13 +1,12 @@
import { Kysely, PostgresDialect } from "kysely"; import { Kysely, PostgresDialect } from "kysely";
import { PostgresIntrospector } from "./PostgresIntrospector"; import { PostgresIntrospector } from "./PostgresIntrospector";
import { PostgresConnection, plugins } from "./PostgresConnection"; import { PostgresConnection, plugins } from "./PostgresConnection";
import { customIntrospector } from "bknd"; import { customIntrospector } from "bknd/data";
import $pg from "pg"; import $pg from "pg";
export type PgPostgresConnectionConfig = $pg.PoolConfig; export type PgPostgresConnectionConfig = $pg.PoolConfig;
export class PgPostgresConnection extends PostgresConnection { export class PgPostgresConnection extends PostgresConnection {
override name = "pg";
private pool: $pg.Pool; private pool: $pg.Pool;
constructor(config: PgPostgresConnectionConfig) { constructor(config: PgPostgresConnectionConfig) {
+9 -13
View File
@@ -1,11 +1,4 @@
import { import { Connection, type DbFunctions, type FieldSpec, type SchemaResponse } from "bknd/data";
Connection,
type DbFunctions,
type FieldSpec,
type SchemaResponse,
type ConnQuery,
type ConnQueryResults,
} from "bknd";
import { import {
ParseJSONResultsPlugin, ParseJSONResultsPlugin,
type ColumnDataType, type ColumnDataType,
@@ -20,13 +13,12 @@ export type QB = SelectQueryBuilder<any, any, any>;
export const plugins = [new ParseJSONResultsPlugin()]; export const plugins = [new ParseJSONResultsPlugin()];
export abstract class PostgresConnection extends Connection { export abstract class PostgresConnection<DB = any> extends Connection<DB> {
protected override readonly supported = { protected override readonly supported = {
batching: true, batching: true,
softscans: true,
}; };
constructor(kysely: Kysely<any>, fn?: Partial<DbFunctions>, _plugins?: KyselyPlugin[]) { constructor(kysely: Kysely<DB>, fn?: Partial<DbFunctions>, _plugins?: KyselyPlugin[]) {
super( super(
kysely, kysely,
fn ?? { fn ?? {
@@ -81,9 +73,13 @@ export abstract class PostgresConnection extends Connection {
]; ];
} }
override async executeQueries<O extends ConnQuery[]>(...qbs: O): Promise<ConnQueryResults<O>> { protected override async batch<Queries extends QB[]>(
queries: [...Queries],
): Promise<{
[K in keyof Queries]: Awaited<ReturnType<Queries[K]["execute"]>>;
}> {
return this.kysely.transaction().execute(async (trx) => { return this.kysely.transaction().execute(async (trx) => {
return Promise.all(qbs.map((q) => trx.executeQuery(q))); return Promise.all(queries.map((q) => trx.executeQuery(q).then((r) => r.rows)));
}) as any; }) as any;
} }
} }
@@ -1,5 +1,5 @@
import { type SchemaMetadata, sql } from "kysely"; import { type SchemaMetadata, sql } from "kysely";
import { BaseIntrospector } from "bknd"; import { BaseIntrospector } from "bknd/data";
type PostgresSchemaSpec = { type PostgresSchemaSpec = {
name: string; name: string;
@@ -1,15 +1,13 @@
import { Kysely } from "kysely"; import { Kysely } from "kysely";
import { PostgresIntrospector } from "./PostgresIntrospector"; import { PostgresIntrospector } from "./PostgresIntrospector";
import { PostgresConnection, plugins } from "./PostgresConnection"; import { PostgresConnection, plugins } from "./PostgresConnection";
import { customIntrospector } from "bknd"; import { customIntrospector } from "bknd/data";
import { PostgresJSDialect } from "kysely-postgres-js"; import { PostgresJSDialect } from "kysely-postgres-js";
import $postgresJs, { type Sql, type Options, type PostgresType } from "postgres"; import $postgresJs, { type Sql, type Options, type PostgresType } from "postgres";
export type PostgresJsConfig = Options<Record<string, PostgresType>>; export type PostgresJsConfig = Options<Record<string, PostgresType>>;
export class PostgresJsConnection extends PostgresConnection { export class PostgresJsConnection extends PostgresConnection {
override name = "postgres-js";
private postgres: Sql; private postgres: Sql;
constructor(opts: { postgres: Sql }) { constructor(opts: { postgres: Sql }) {
+5 -8
View File
@@ -1,10 +1,9 @@
import { customIntrospector, type DbFunctions } from "bknd"; import type { Constructor } from "bknd/core";
import { customIntrospector, type DbFunctions } from "bknd/data";
import { Kysely, type Dialect, type KyselyPlugin } from "kysely"; import { Kysely, type Dialect, type KyselyPlugin } from "kysely";
import { plugins, PostgresConnection } from "./PostgresConnection"; import { plugins, PostgresConnection } from "./PostgresConnection";
import { PostgresIntrospector } from "./PostgresIntrospector"; import { PostgresIntrospector } from "./PostgresIntrospector";
export type Constructor<T> = new (...args: any[]) => T;
export type CustomPostgresConnection = { export type CustomPostgresConnection = {
supports?: PostgresConnection["supported"]; supports?: PostgresConnection["supported"];
fn?: Partial<DbFunctions>; fn?: Partial<DbFunctions>;
@@ -16,19 +15,17 @@ export function createCustomPostgresConnection<
T extends Constructor<Dialect>, T extends Constructor<Dialect>,
C extends ConstructorParameters<T>[0], C extends ConstructorParameters<T>[0],
>( >(
name: string,
dialect: Constructor<Dialect>, dialect: Constructor<Dialect>,
options?: CustomPostgresConnection, options?: CustomPostgresConnection,
): (config: C) => PostgresConnection { ): (config: C) => PostgresConnection<any> {
const supported = { const supported = {
batching: true, batching: true,
...((options?.supports ?? {}) as any), ...((options?.supports ?? {}) as any),
}; };
return (config: C) => return (config: C) =>
new (class extends PostgresConnection { new (class extends PostgresConnection<any> {
override name = name; protected override readonly supported = supported;
override readonly supported = supported;
constructor(config: C) { constructor(config: C) {
super( super(
+17 -38
View File
@@ -1,11 +1,8 @@
import { describe, beforeAll, afterAll, expect, it, afterEach } from "bun:test"; import { describe, beforeAll, afterAll, expect, it, afterEach } from "bun:test";
import type { PostgresConnection } from "../src"; import type { PostgresConnection } from "../src";
import { createApp, em, entity, text } from "bknd"; import { createApp } from "bknd";
import * as proto from "bknd/data";
import { disableConsoleLog, enableConsoleLog } from "bknd/utils"; import { disableConsoleLog, enableConsoleLog } from "bknd/utils";
// @ts-ignore
import { connectionTestSuite } from "$bknd/data/connection/connection-test-suite";
// @ts-ignore
import { bunTestRunner } from "$bknd/adapter/bun/test";
export type TestSuiteConfig = { export type TestSuiteConfig = {
createConnection: () => InstanceType<typeof PostgresConnection>; createConnection: () => InstanceType<typeof PostgresConnection>;
@@ -15,9 +12,8 @@ export type TestSuiteConfig = {
export async function defaultCleanDatabase(connection: InstanceType<typeof PostgresConnection>) { export async function defaultCleanDatabase(connection: InstanceType<typeof PostgresConnection>) {
const kysely = connection.kysely; const kysely = connection.kysely;
// drop all tables+indexes & create new schema // drop all tables & create new schema
await kysely.schema.dropSchema("public").ifExists().cascade().execute(); await kysely.schema.dropSchema("public").ifExists().cascade().execute();
await kysely.schema.dropIndex("public").ifExists().cascade().execute();
await kysely.schema.createSchema("public").execute(); await kysely.schema.createSchema("public").execute();
} }
@@ -36,23 +32,6 @@ export function testSuite(config: TestSuiteConfig) {
beforeAll(() => disableConsoleLog(["log", "warn", "error"])); beforeAll(() => disableConsoleLog(["log", "warn", "error"]));
afterAll(() => enableConsoleLog()); afterAll(() => enableConsoleLog());
// @todo: postgres seems to add multiple indexes, thus failing the test suite
/* describe("test suite", () => {
connectionTestSuite(bunTestRunner, {
makeConnection: () => {
const connection = config.createConnection();
return {
connection,
dispose: async () => {
await cleanDatabase(connection, config);
await connection.close();
},
};
},
rawDialectDetails: [],
});
}); */
describe("base", () => { describe("base", () => {
it("should connect to the database", async () => { it("should connect to the database", async () => {
const connection = config.createConnection(); const connection = config.createConnection();
@@ -94,14 +73,14 @@ export function testSuite(config: TestSuiteConfig) {
}); });
it("should create a basic schema", async () => { it("should create a basic schema", async () => {
const schema = em( const schema = proto.em(
{ {
posts: entity("posts", { posts: proto.entity("posts", {
title: text().required(), title: proto.text().required(),
content: text(), content: proto.text(),
}), }),
comments: entity("comments", { comments: proto.entity("comments", {
content: text(), content: proto.text(),
}), }),
}, },
(fns, s) => { (fns, s) => {
@@ -174,20 +153,20 @@ export function testSuite(config: TestSuiteConfig) {
}); });
it("should support uuid", async () => { it("should support uuid", async () => {
const schema = em( const schema = proto.em(
{ {
posts: entity( posts: proto.entity(
"posts", "posts",
{ {
title: text().required(), title: proto.text().required(),
content: text(), content: proto.text(),
}, },
{ {
primary_format: "uuid", primary_format: "uuid",
}, },
), ),
comments: entity("comments", { comments: proto.entity("comments", {
content: text(), content: proto.text(),
}), }),
}, },
(fns, s) => { (fns, s) => {
@@ -208,8 +187,8 @@ export function testSuite(config: TestSuiteConfig) {
// @ts-expect-error // @ts-expect-error
expect(config.data.entities?.posts.fields?.id.config?.format).toBe("uuid"); expect(config.data.entities?.posts.fields?.id.config?.format).toBe("uuid");
const $em = app.em; const em = app.em;
const mutator = $em.mutator($em.entity("posts")); const mutator = em.mutator(em.entity("posts"));
const data = await mutator.insertOne({ title: "Hello", content: "World" }); const data = await mutator.insertOne({ title: "Hello", content: "World" });
expect(data.data.id).toBeString(); expect(data.data.id).toBeString();
expect(String(data.data.id).length).toBe(36); expect(String(data.data.id).length).toBe(36);
+27 -31
View File
@@ -1,33 +1,29 @@
{ {
"compilerOptions": { "compilerOptions": {
"composite": false, "composite": false,
"module": "ESNext", "module": "ESNext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": false, "allowImportingTsExtensions": false,
"target": "ES2022", "target": "ES2022",
"noImplicitAny": false, "noImplicitAny": false,
"allowJs": true, "allowJs": true,
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
"declaration": true, "declaration": true,
"strict": true, "strict": true,
"allowUnusedLabels": false, "allowUnusedLabels": false,
"allowUnreachableCode": false, "allowUnreachableCode": false,
"exactOptionalPropertyTypes": false, "exactOptionalPropertyTypes": false,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noImplicitOverride": true, "noImplicitOverride": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"noPropertyAccessFromIndexSignature": false, "noPropertyAccessFromIndexSignature": false,
"noUncheckedIndexedAccess": true, "noUncheckedIndexedAccess": true,
"noUnusedLocals": false, "noUnusedLocals": false,
"noUnusedParameters": false, "noUnusedParameters": false,
"isolatedModules": true, "isolatedModules": true,
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true, "skipLibCheck": true
"baseUrl": ".", },
"paths": { "include": ["./src/**/*.ts"],
"$bknd/*": ["../../app/src/*"] "exclude": ["node_modules"]
}
},
"include": ["./src/**/*.ts"],
"exclude": ["node_modules"]
} }