Compare commits

...

28 Commits

Author SHA1 Message Date
dswbx 7f8d8d26ac Merge pull request #259 from bknd-io/release/0.17.2
Bugfix Release 0.17.2
2025-09-15 10:15:14 +02:00
dswbx 655bb241a8 bump version to 0.17.2 2025-09-15 10:14:14 +02:00
dswbx 1ef5bf7d15 fix: adjust mcp schema validation and add default path support
Updated `mcp.json` to enhance validation by supporting `anyOf` schema for `entity` properties and refined `role` typing. Introduced default `path` property for MCP system API and aligned build assets configuration accordingly.
2025-09-15 10:09:51 +02:00
dswbx 9a1589da0b bump @bknd/postgres version to 0.2.0 2025-09-15 10:07:34 +02:00
dswbx f231353228 bump version to 0.17.2-rc.1 2025-09-15 09:31:01 +02:00
dswbx d2de642c17 fix: update build config and improve e2e adapter handling
Added dynamic inclusion of dependencies as external packages and switched build platform to browser. Streamlined e2e adapter test setup, improved type safety, and standardized console output for better clarity.
2025-09-15 09:25:32 +02:00
dswbx 50c5383138 Merge pull request #257 from bknd-io/fix/postgres-0.17-updates
postgres: bump 0.17.1 and improve custom connection API
2025-09-14 17:10:32 +02:00
dswbx fff88a9237 Merge pull request #258 from bknd-io/fix/mcp-auth-and-insert
fix: handle numbered object conversion and update MCP tool URL
2025-09-14 17:09:55 +02:00
dswbx d768a80526 Merge pull request #256 from bknd-io/fix/docker-0.17
bump Dockerfile to use bknd 0.17.1
2025-09-14 17:09:34 +02:00
dswbx 0d79b19c66 Merge pull request #255 from bknd-io/fix/node-static-import
fix raw node execution by making sure import attributes stick on builds
2025-09-14 17:08:54 +02:00
dswbx fea2812688 fix: handle numbered object conversion and update MCP tool URL
Add `convertNumberedObjectToArray` utility for handling numbered object to array conversion, addressing MCP tool allOf behavior. Adjust MCP tool URL in configuration and ensure default inspect options in development environment. Minor improvement in role enumeration handling for auth.
2025-09-14 17:03:23 +02:00
dswbx 62368c691a postgres: bump 0.17.1 and improve custom connection API
Aligned connection constructors to include an explicit name parameter, updated documentation, and streamlined connection methods for consistency. Adjusted dependencies and cleaned unused references.
2025-09-14 16:01:37 +02:00
dswbx dd9617598f bump Dockerfile to use bknd 0.17.1
Updated the default bknd version to 0.17.1 and included the `jsonv-ts` dependency in the Dockerfile.
2025-09-14 14:07:56 +02:00
dswbx a7f6d45ca9 fix raw node execution by making sure import attributes stick on builds 2025-09-14 13:43:45 +02:00
dswbx 92656523ff Merge pull request #254 from quicujo/feat/validate-uuid
Add UUID validation utility
2025-09-14 13:19:57 +02:00
anton e6ef7d9ff4 Add UUID validation utility
Add uuidValidate function to validate UUID strings and check their version.
Supports validation for v4 and v7 UUIDs using the uuid library's validate
and version functions.
2025-09-08 05:06:42 +02:00
dswbx 210b22e307 Merge pull request #251 from bknd-io/fix/cf-adapter-exports
cloudflare: fix missing serve export
2025-09-05 09:18:36 +02:00
dswbx 734235c798 cloudflare: fix missing serve export 2025-09-05 09:17:08 +02:00
dswbx c9773d49a6 bump 0.17.1 2025-09-03 17:17:26 +02:00
dswbx c9fb497c2d Merge pull request #246 from bknd-io/fix/mobile-input-zoom
style: fix mobile zoom on input focus/blur
2025-09-03 17:03:14 +02:00
dswbx ccd4eb10cb Merge pull request #245 from bknd-io/fix/cf-instance-flash
cloudflare: fixing multiple instances competing with configuration state by always serving fresh
2025-09-03 17:03:03 +02:00
dswbx 847f75b725 docs: update cloudflare imports 2025-09-03 17:00:24 +02:00
dswbx 8b4bf0d94a style: fix mobile zoom on input focus/blur 2025-09-03 08:28:27 +02:00
dswbx bf521e2931 cloudflare: fixing multiple instances competing with configuration state by always serving fresh 2025-09-03 07:54:40 +02:00
dswbx 111b2ae364 docs: fix generated mcp tools page title 2025-08-30 17:31:22 +02:00
dswbx dc08d12ce6 docs & readme: fix demo 2025-08-30 16:21:46 +02:00
dswbx ce773007f0 readme: update badges 2025-08-30 15:05:46 +02:00
dswbx e78c271c2f readme: added mcp as feature 2025-08-30 15:03:46 +02:00
50 changed files with 461 additions and 445 deletions
+1 -4
View File
@@ -1,10 +1,7 @@
{
"mcpServers": {
"bknd": {
"url": "http://localhost:3000/mcp",
"headers": {
"API_KEY": "value"
}
"url": "http://localhost:28623/api/system/mcp"
}
}
}
+5 -4
View File
@@ -3,7 +3,7 @@
![bknd](https://raw.githubusercontent.com/bknd-io/bknd/refs/heads/main/docs/public/assets/poster.png)
<p align="center" width="100%">
<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">
<a href="https://stackblitz.com/github/bknd-io/bknd-demo?hideExplorer=1&embed=1&view=preview&initialPath=%2Fdata%2Fschema" target="_blank">
<strong>⭐ Live Demo</strong>
</a>
</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.**
> [!WARNING]
> This project requires Node.js 22 or higher (because of `node:sqlite`).
> This project requires Node.js 22.13 or higher (because of `node:sqlite`).
>
> Please keep in mind that **bknd** is still under active development
> 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
![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/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)
![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/ui](https://img.badgesize.io/https://unpkg.com/bknd@latest/dist/ui/index.js?compression=gzip&label=bknd/ui&t=1)
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,6 +52,7 @@ Creating digital products always requires developing both the backend (the logic
* using a React framework (Next.js, React Router, Astro)
* 📦 Official API and React SDK with type-safety
* ⚛️ React elements for auto-configured authentication and media components
* 🛠️ MCP server, client and UI built-in to control your backend
## Structure
The package is mainly split into 4 parts, each serving a specific purpose:
+6 -1
View File
@@ -61,8 +61,11 @@ function delayTypes() {
watcher_timeout = setTimeout(buildTypes, 1000);
}
const dependencies = Object.keys(pkg.dependencies);
// collection of always-external packages
const external = [
...dependencies,
"bun:test",
"node:test",
"node:assert/strict",
@@ -86,10 +89,10 @@ async function buildApi() {
outDir: "dist",
external: [...external],
metafile: true,
target: "esnext",
platform: "browser",
format: ["esm"],
splitting: false,
treeshake: true,
loader: {
".svg": "dataurl",
},
@@ -245,6 +248,8 @@ async function buildAdapters() {
// base adapter handles
tsup.build({
...baseConfig(""),
target: "esnext",
platform: "neutral",
entry: ["src/adapter/index.ts"],
outDir: "dist/adapter",
}),
+6 -6
View File
@@ -17,7 +17,7 @@ async function run(
});
// Read from stdout
const reader = proc.stdout.getReader();
const reader = (proc.stdout as ReadableStream).getReader();
const decoder = new TextDecoder();
// Function to read chunks
@@ -30,7 +30,7 @@ async function run(
const text = decoder.decode(value);
if (!resolveCalled) {
console.log(c.dim(text.replace(/\n$/, "")));
console.info(c.dim(text.replace(/\n$/, "")));
}
onChunk(
text,
@@ -189,21 +189,21 @@ const adapters = {
async function testAdapter(name: keyof typeof adapters) {
const config = adapters[name];
console.log("adapter", c.cyan(name));
console.info("adapter", c.cyan(name));
await config.clean();
const { proc, data } = await config.start();
console.log("proc:", proc.pid, "data:", c.cyan(data));
console.info("proc:", proc.pid, "data:", c.cyan(data));
//proc.kill();process.exit(0);
const add_env = "env" in config && config.env ? config.env : "";
await $`TEST_URL=${data} TEST_ADAPTER=${name} ${add_env} bun run test:e2e`;
console.log("DONE!");
console.info("DONE!");
while (!proc.killed) {
proc.kill("SIGINT");
await Bun.sleep(250);
console.log("Waiting for process to exit...");
console.info("Waiting for process to exit...");
}
}
+1
View File
@@ -7,6 +7,7 @@ async function generate() {
server: {
mcp: {
enabled: true,
path: "/mcp",
},
},
auth: {
+6 -6
View File
@@ -3,7 +3,7 @@
"type": "module",
"sideEffects": false,
"bin": "./dist/cli/index.js",
"version": "0.17.0",
"version": "0.17.2",
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
"homepage": "https://bknd.io",
"repository": {
@@ -15,7 +15,7 @@
},
"packageManager": "bun@1.2.19",
"engines": {
"node": ">=22"
"node": ">=22.13"
},
"scripts": {
"dev": "BKND_CLI_LOG_LEVEL=debug vite",
@@ -30,7 +30,7 @@
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly && tsc-alias",
"updater": "bun x npm-check-updates -ui",
"cli": "LOCAL=1 bun src/cli/index.ts",
"prepublishOnly": "bun run types && bun run test && bun run test:node && bun run test:e2e && bun run build:all && cp ../README.md ./",
"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 ./",
"postpublish": "rm -f README.md",
"test": "ALL_TESTS=1 bun test --bail",
"test:all": "bun run test && bun run test:node",
@@ -39,8 +39,8 @@
"test:adapters": "bun test src/adapter/**/*.adapter.spec.ts --bail",
"test:coverage": "ALL_TESTS=1 bun test --bail --coverage",
"test:vitest:coverage": "vitest run --coverage",
"test:e2e": "VITE_DB_URL=:memory: playwright test",
"test:e2e:adapters": "VITE_DB_URL=:memory: bun run e2e/adapters.ts",
"test:e2e": "playwright test",
"test:e2e:adapters": "bun run e2e/adapters.ts",
"test:e2e:ui": "VITE_DB_URL=:memory: playwright test --ui",
"test:e2e:debug": "VITE_DB_URL=:memory: playwright test --debug",
"test:e2e:report": "VITE_DB_URL=:memory: playwright show-report",
@@ -71,6 +71,7 @@
"oauth4webapi": "^2.11.1",
"object-path-immutable": "^4.1.2",
"radix-ui": "^1.1.3",
"picocolors": "^1.1.1",
"swr": "^2.3.3"
},
"devDependencies": {
@@ -108,7 +109,6 @@
"libsql-stateless-easy": "^1.8.0",
"open": "^10.1.0",
"openapi-types": "^12.1.3",
"picocolors": "^1.1.1",
"postcss": "^8.5.3",
"postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1",
+1 -1
View File
@@ -43,7 +43,7 @@ export type ApiOptions = {
} & (
| {
token?: string;
user?: TApiUser;
user?: TApiUser | null;
}
| {
request: Request;
+2 -2
View File
@@ -53,7 +53,7 @@ export function adapterTestSuite<
url: overrides.dbUrl ?? ":memory:",
origin: "localhost",
} as any,
{ id },
{ force: false, id },
);
expect(app).toBeDefined();
expect(app.toJSON().server.cors.origin).toEqual("localhost");
@@ -69,7 +69,7 @@ export function adapterTestSuite<
};
test("responds with the same app id", async () => {
const fetcher = makeHandler(undefined, undefined, { id });
const fetcher = makeHandler(undefined, undefined, { force: false, id });
const { res, data } = await getConfig(fetcher);
expect(res.ok).toBe(true);
@@ -1,10 +1,9 @@
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
import { makeApp } from "./modes/fresh";
import { makeConfig, type CfMakeConfigArgs } from "./config";
import { makeConfig, type CloudflareContext } from "./config";
import { disableConsoleLog, enableConsoleLog } from "core/utils";
import { adapterTestSuite } from "adapter/adapter-test-suite";
import { bunTestRunner } from "adapter/bun/test";
import type { CloudflareBkndConfig } from "./cloudflare-workers.adapter";
import { type CloudflareBkndConfig, createApp } from "./cloudflare-workers.adapter";
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
@@ -41,18 +40,19 @@ describe("cf adapter", () => {
expect(dynamicConfig.connection).toBeDefined();
});
adapterTestSuite<CloudflareBkndConfig, CfMakeConfigArgs<any>>(bunTestRunner, {
adapterTestSuite<CloudflareBkndConfig, CloudflareContext<any>>(bunTestRunner, {
makeApp: async (c, a, o) => {
return await makeApp(c, { env: a } as any, o);
return await createApp(c, { env: a } as any, o);
},
makeHandler: (c, a, o) => {
console.log("args", a);
return async (request: any) => {
const app = await makeApp(
const app = await createApp(
// needs a fallback, otherwise tries to launch D1
c ?? {
connection: { url: DB_URL },
},
a!,
a as any,
o,
);
return app.fetch(request);
@@ -3,10 +3,10 @@
import type { RuntimeBkndConfig } from "bknd/adapter";
import { Hono } from "hono";
import { serveStatic } from "hono/cloudflare-workers";
import { getFresh } from "./modes/fresh";
import { getCached } from "./modes/cached";
import type { App, MaybePromise } from "bknd";
import type { MaybePromise } from "bknd";
import { $console } from "bknd/utils";
import { createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
import { registerAsyncsExecutionContext, makeConfig, type CloudflareContext } from "./config";
declare global {
namespace Cloudflare {
@@ -16,7 +16,6 @@ declare global {
export type CloudflareEnv = Cloudflare.Env;
export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> & {
mode?: "warm" | "fresh" | "cache";
bindings?: (args: Env) => MaybePromise<{
kv?: KVNamespace;
db?: D1Database;
@@ -34,11 +33,31 @@ export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> &
registerMedia?: boolean | ((env: Env) => void);
};
export type Context<Env = CloudflareEnv> = {
request: Request;
env: Env;
ctx: ExecutionContext;
};
export async function createApp<Env extends CloudflareEnv = CloudflareEnv>(
config: CloudflareBkndConfig<Env>,
ctx: Partial<CloudflareContext<Env>> = {},
opts: RuntimeOptions = {
// 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>(
config: CloudflareBkndConfig<Env> = {},
@@ -77,23 +96,8 @@ export function serve<Env extends CloudflareEnv = CloudflareEnv>(
}
}
const context = { request, env, ctx } as Context<Env>;
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}`);
}
const context = { request, env, ctx } as CloudflareContext<Env>;
const app = await createApp(config, context);
return app.fetch(request, env, ctx);
},
+6 -6
View File
@@ -8,7 +8,7 @@ import { getBinding } from "./bindings";
import { d1Sqlite } from "./connection/D1Connection";
import type { CloudflareBkndConfig, CloudflareEnv } from ".";
import { App } from "bknd";
import type { Context, ExecutionContext } from "hono";
import type { Context as HonoContext, ExecutionContext } from "hono";
import { $console } from "bknd/utils";
import { setCookie } from "hono/cookie";
@@ -22,10 +22,10 @@ export const constants = {
},
};
export type CfMakeConfigArgs<Env extends CloudflareEnv = CloudflareEnv> = {
export type CloudflareContext<Env extends CloudflareEnv = CloudflareEnv> = {
env: Env;
ctx?: ExecutionContext;
request?: Request;
ctx: ExecutionContext;
request: Request;
};
function getCookieValue(cookies: string | null, name: string) {
@@ -67,7 +67,7 @@ export function d1SessionHelper(config: CloudflareBkndConfig<any>) {
return undefined;
},
set: (c: Context, d1?: D1DatabaseSession) => {
set: (c: HonoContext, d1?: D1DatabaseSession) => {
if (!d1 || !config.d1?.session) return;
const session = d1.getBookmark();
@@ -91,7 +91,7 @@ export function d1SessionHelper(config: CloudflareBkndConfig<any>) {
let media_registered: boolean = false;
export async function makeConfig<Env extends CloudflareEnv = CloudflareEnv>(
config: CloudflareBkndConfig<Env>,
args?: CfMakeConfigArgs<Env>,
args?: Partial<CloudflareContext<Env>>,
) {
if (!media_registered && config.registerMedia !== false) {
if (typeof config.registerMedia === "function") {
+8 -4
View File
@@ -1,8 +1,12 @@
import { d1Sqlite, type D1ConnectionConfig } from "./connection/D1Connection";
export * from "./cloudflare-workers.adapter";
export { makeApp, getFresh } from "./modes/fresh";
export { getCached } from "./modes/cached";
export {
getFresh,
createApp,
serve,
type CloudflareEnv,
type CloudflareBkndConfig,
} from "./cloudflare-workers.adapter";
export { d1Sqlite, type D1ConnectionConfig };
export { doSqlite, type DoConnectionConfig } from "./connection/DoConnection";
export {
@@ -12,7 +16,7 @@ export {
type GetBindingType,
type BindingMap,
} from "./bindings";
export { constants } from "./config";
export { constants, type CloudflareContext } from "./config";
export { StorageR2Adapter, registerMedia } from "./storage/StorageR2Adapter";
export { registries } from "bknd";
export { devFsVitePlugin, devFsWrite } from "./vite";
@@ -1,53 +0,0 @@
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
@@ -1,29 +0,0 @@
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;
configFile?: string;
}): Plugin {
} = {}): any {
let isDev = false;
let projectRoot = "";
@@ -115,7 +115,7 @@ if (typeof globalThis !== 'undefined') {
return polyfill + code;
}
},
};
} satisfies Plugin;
}
// Write function that uses the dev-fs polyfill injected by our Vite plugin
+9 -3
View File
@@ -83,8 +83,12 @@ export async function createAdapterApp<Config extends BkndConfig = BkndConfig, A
}
app = App.create(appConfig);
apps.set(id, app);
if (!opts?.force) {
apps.set(id, app);
}
}
return app;
}
@@ -169,7 +173,9 @@ export function serveStaticViaImport(opts?: { manifest?: Manifest }) {
return async (c: Context, next: Next) => {
if (!files) {
const manifest =
opts?.manifest || ((await import("bknd/dist/manifest.json")).default as Manifest);
opts?.manifest ||
((await import("bknd/dist/manifest.json", { with: { type: "json" } }))
.default as Manifest);
files = Object.values(manifest).flatMap((asset) => [asset.file, ...(asset.css || [])]);
}
@@ -177,7 +183,7 @@ export function serveStaticViaImport(opts?: { manifest?: Manifest }) {
if (files.includes(path)) {
try {
const content = await import(/* @vite-ignore */ `bknd/static/${path}?raw`, {
assert: { type: "text" },
with: { type: "text" },
}).then((m) => m.default);
if (content) {
+2 -1
View File
@@ -221,6 +221,7 @@ export class AuthController extends Controller {
return user;
};
const roles = Object.keys(this.auth.config.roles ?? {});
mcp.tool(
// @todo: needs permission
"auth_user_create",
@@ -231,7 +232,7 @@ export class AuthController extends Controller {
password: s.string({ minLength: 8 }),
role: s
.string({
enum: Object.keys(this.auth.config.roles ?? {}),
enum: roles.length > 0 ? roles : undefined,
})
.optional(),
}),
+7
View File
@@ -473,3 +473,10 @@ export function deepFreeze<T extends object>(object: T): T {
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;
}
+9 -3
View File
@@ -1,10 +1,16 @@
import { v4, v7 } from "uuid";
import { v4, v7, validate, version as uuidVersion } from "uuid";
// generates v4
export function uuid(): string {
return v4();
return v4();
}
// generates v7
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;
}
+18 -15
View File
@@ -1,7 +1,15 @@
import type { Handler } from "hono/types";
import type { ModuleBuildContext } from "modules";
import { Controller } from "modules/Controller";
import { jsc, s, describeRoute, schemaToSpec, omitKeys, pickKeys, mcpTool } from "bknd/utils";
import {
jsc,
s,
describeRoute,
schemaToSpec,
omitKeys,
pickKeys,
mcpTool,
convertNumberedObjectToArray,
} from "bknd/utils";
import * as SystemPermissions from "modules/permissions";
import type { AppDataConfig } from "../data-schema";
import type { EntityManager, EntityData } from "data/entities";
@@ -37,14 +45,6 @@ export class DataController extends Controller {
const hono = this.create().use(auth(), permission(SystemPermissions.accessApi));
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
hono.get(
"/",
@@ -52,10 +52,7 @@ export class DataController extends Controller {
summary: "Retrieve data configuration",
tags: ["data"],
}),
handler("data info", (c) => {
// sample implementation
return c.json(this.em.toJSON());
}),
(c) => c.json(this.em.toJSON()),
);
// sync endpoint
@@ -432,7 +429,13 @@ export class DataController extends Controller {
if (!this.entityExists(entity)) {
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)) {
const result = await this.em.mutator(entity).insertMany(body);
+3
View File
@@ -258,6 +258,9 @@ export class EntityManager<TBD extends object = DefaultDB> {
// @todo: centralize and add tests
hydrate(entity_name: string, _data: EntityData[]) {
if (!Array.isArray(_data) || _data.length === 0) {
return [];
}
const entity = this.entity(entity_name);
const data: EntityData[] = [];
+1 -1
View File
@@ -51,7 +51,7 @@ export class Controller {
protected getEntitiesEnum(em: EntityManager<any>): s.StringSchema {
const entities = em.entities.map((e) => e.name);
return entities.length > 0 ? s.string({ enum: entities }) : s.string();
return entities.length > 0 ? s.anyOf([s.string({ enum: entities }), s.string()]) : s.string();
}
registerMcp(): void {}
+9 -5
View File
@@ -11,6 +11,7 @@ import { css, Style } from "hono/css";
import { Controller } from "modules/Controller";
import * as SystemPermissions from "modules/permissions";
import type { TApiUser } from "Api";
import type { Manifest } from "vite";
const htmlBkndContextReplace = "<!-- BKND_CONTEXT -->";
@@ -32,6 +33,7 @@ export type AdminControllerOptions = {
debugRerenders?: boolean;
theme?: "dark" | "light" | "system";
logoReturnPath?: string;
manifest?: Manifest;
};
export class AdminController extends Controller {
@@ -194,8 +196,10 @@ export class AdminController extends Controller {
};
if (isProd) {
let manifest: any;
if (this.options.assetsPath.startsWith("http")) {
let manifest: Manifest;
if (this.options.manifest) {
manifest = this.options.manifest;
} else if (this.options.assetsPath.startsWith("http")) {
manifest = await fetch(this.options.assetsPath + ".vite/manifest.json", {
headers: {
Accept: "application/json",
@@ -204,14 +208,14 @@ export class AdminController extends Controller {
} else {
// @ts-ignore
manifest = await import("bknd/dist/manifest.json", {
assert: { type: "json" },
with: { type: "json" },
}).then((res) => res.default);
}
try {
// @todo: load all marked as entry (incl. css)
assets.js = manifest["src/ui/main.tsx"].file;
assets.css = manifest["src/ui/main.tsx"].css[0] as any;
assets.js = manifest["src/ui/main.tsx"]?.file!;
assets.css = manifest["src/ui/main.tsx"]?.css?.[0] as any;
} catch (e) {
$console.warn("Couldn't find assets in manifest", e);
}
@@ -376,6 +376,7 @@ export class SystemController extends Controller {
}),
(c) =>
c.json({
id: this.app._id,
version: {
config: c.get("app")?.version(),
bknd: getVersion(),
@@ -16,7 +16,7 @@ const schema = s.partialObject({
metadata: s.string({ enum: ["copyright", "keep", "none"] }),
quality: s.number({ minimum: 1, maximum: 100 }),
});
type ImageOptimizationSchema = s.Static<typeof schema>;
export type CloudflareImageOptimizationSchema = s.Static<typeof schema>;
export type CloudflareImageOptimizationOptions = {
/**
@@ -38,12 +38,12 @@ export type CloudflareImageOptimizationOptions = {
* The default options to use
* @default {}
*/
defaultOptions?: ImageOptimizationSchema;
defaultOptions?: CloudflareImageOptimizationSchema;
/**
* The fixed options to use
* @default {}
*/
fixedOptions?: ImageOptimizationSchema;
fixedOptions?: CloudflareImageOptimizationSchema;
/**
* The cache control to use
* @default public, max-age=31536000, immutable
+1
View File
@@ -1,5 +1,6 @@
export {
cloudflareImageOptimization,
type CloudflareImageOptimizationSchema,
type CloudflareImageOptimizationOptions,
} from "./cloudflare/image-optimization.plugin";
export { showRoutes, type ShowRoutesOptions } from "./dev/show-routes.plugin";
+9 -4
View File
@@ -397,11 +397,16 @@ export function MaxHeightContainer(props: ComponentPropsWithoutRef<"div">) {
}
}
useEffect(updateHeaderHeight, []);
useEffect(() => {
updateHeaderHeight();
const resize = throttle(updateHeaderHeight, 500);
if (typeof window !== "undefined") {
window.addEventListener("resize", throttle(updateHeaderHeight, 500));
}
window.addEventListener("resize", resize);
return () => {
window.removeEventListener("resize", resize);
};
}, []);
return (
<div ref={scrollRef} style={{ height: `${height - offset}px` }} {...props}>
+4
View File
@@ -122,3 +122,7 @@ body,
@utility debug-blue {
@apply border-blue-500 border;
}
@utility w-min-content {
width: min-content;
width: -webkit-min-content;
}
+2 -2
View File
@@ -139,7 +139,7 @@ function Adapters() {
<span className="font-bold">Media Adapter:</span>
{ctx.selected === null && <span className="opacity-70"> (Choose one)</span>}
</Formy.Label>
<div className="flex flex-row gap-1 mb-2">
<div className="grid grid-cols-2 md:flex flex-row gap-1 mb-2 flex-wrap">
{ctx.schemas?.map((schema: any, i) => (
<Button
key={i}
@@ -165,7 +165,7 @@ function Adapters() {
</div>
{ctx.selected !== null && (
<Formy.Group as="fieldset" error={ctx.errors.length > 0}>
<Formy.Label as="legend" className="font-mono px-2">
<Formy.Label as="legend" className="font-mono px-2 w-min-content">
{autoFormatString(ctx.selectedSchema!.title!)}
</Formy.Label>
<FormContextOverride schema={ctx.selectedSchema} prefix={ctx.path}>
+4 -2
View File
@@ -8,8 +8,10 @@ html.fixed,
html.fixed body {
top: 0;
left: 0;
height: 100%;
width: 100%;
right: 0;
bottom: 0;
height: 100dvh;
width: 100dvw;
position: fixed;
overflow: hidden;
overscroll-behavior-x: contain;
+3
View File
@@ -9,6 +9,9 @@ import { nodeSqlite } from "./src/adapter/node/connection/NodeSqliteConnection";
import { libsql } from "./src/data/connection/sqlite/libsql/LibsqlConnection";
import { $console } from "core/utils/console";
import { createClient } from "@libsql/client";
import util from "node:util";
util.inspect.defaultOptions.depth = 5;
registries.media.register("local", StorageLocalAdapter);
+3 -10
View File
@@ -15,7 +15,7 @@
},
"app": {
"name": "bknd",
"version": "0.17.0-rc.1",
"version": "0.17.1",
"bin": "./dist/cli/index.js",
"dependencies": {
"@cfworker/json-schema": "^4.1.1",
@@ -151,7 +151,6 @@
"bknd": "workspace:*",
"kysely-neon": "^1.3.0",
"tsup": "^8.4.0",
"typescript": "^5.8.2",
},
"optionalDependencies": {
"kysely": "^0.27.6",
@@ -1232,7 +1231,7 @@
"@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.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="],
"@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
@@ -3832,10 +3831,6 @@
"@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=="],
"@bundled-es-modules/cookie/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
@@ -4078,7 +4073,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=="],
"@types/bun/bun-types": ["bun-types@1.2.20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="],
"@types/bun/bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="],
"@typescript-eslint/experimental-utils/eslint-utils": ["eslint-utils@2.1.0", "", { "dependencies": { "eslint-visitor-keys": "^1.1.0" } }, "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg=="],
@@ -4684,8 +4679,6 @@
"@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=="],
"@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:
# `docker build --build-arg VERSION=<version> -t bknd .`
ARG VERSION=0.13.0
ARG VERSION=0.17.1
# Install & copy required cli
RUN npm install --omit=dev bknd@${VERSION}
@@ -16,10 +16,10 @@ FROM node:24-alpine
WORKDIR /app
# Install pm2 and libsql
# Install required dependencies
RUN npm install -g pm2
RUN echo '{"type":"module"}' > package.json
RUN npm install @libsql/client
RUN npm install jsonv-ts @libsql/client
# Create volume and init args
VOLUME /data
+65 -67
View File
@@ -3,79 +3,77 @@
import * as React from "react";
export const examples = {
adminRich: {
path: "github/bknd-io/bknd-examples",
startScript: "example-admin-rich",
initialPath: "/data/schema"
}
adminRich: {
path: "github/bknd-io/bknd-examples",
startScript: "example-admin-rich",
initialPath: "/data/schema",
},
};
export const StackBlitz = ({
path,
ratio = 9 / 16,
example,
...props
path,
ratio = 9 / 16,
example,
...props
}: {
path?: string;
ratio?: number;
example?: keyof typeof examples;
[key: string]: unknown;
path?: string;
ratio?: number;
example?: keyof typeof examples;
[key: string]: unknown;
}) => {
const selected = example ? examples[example] : undefined;
const finalPath = path || selected?.path || "github/bknd-io/bknd-examples";
const selected = example ? examples[example] : undefined;
const finalPath = path || selected?.path || "github/bknd-io/bknd-examples";
const params = new URLSearchParams({
ctl: "1",
hideExplorer: "1",
embed: "1",
view: "preview",
...(selected || {}),
...props
});
const params = new URLSearchParams({
ctl: "1",
hideExplorer: "1",
embed: "1",
view: "preview",
...(selected || {}),
...props,
});
const url = new URL(
`https://stackblitz.com/${finalPath}?${params.toString()}`
);
const url = new URL(`https://stackblitz.com/${finalPath}?${params.toString()}`);
return (
<>
<div
style={{
position: "relative",
overflow: "hidden",
width: "100%",
paddingTop: `${ratio * 100}%`
}}
>
<iframe
width="100%"
height="100%"
src={url.toString()}
style={{
position: "absolute",
top: 0,
left: 0,
bottom: 0,
right: 0,
border: "none"
}}
/>
</div>
<div
style={{
fontSize: "80%",
opacity: 0.7,
marginTop: "0.2rem",
marginBottom: "1rem",
textAlign: "center"
}}
>
If you&rsquo;re having issues viewing it inline,{" "}
<a href={url.toString()} target="_blank" rel="noreferrer">
try in a new tab
</a>
.
</div>
</>
);
return (
<>
<div
style={{
position: "relative",
overflow: "hidden",
width: "100%",
paddingTop: `${ratio * 100}%`,
}}
>
<iframe
width="100%"
height="100%"
src={url.toString()}
style={{
position: "absolute",
top: 0,
left: 0,
bottom: 0,
right: 0,
border: "none",
}}
/>
</div>
<div
style={{
fontSize: "80%",
opacity: 0.7,
marginTop: "0.2rem",
marginBottom: "1rem",
textAlign: "center",
}}
>
If you&rsquo;re having issues viewing it inline,{" "}
<a href={url.toString()} target="_blank" rel="noreferrer">
try in a new tab
</a>
.
</div>
</>
);
};
@@ -42,8 +42,7 @@ bun add bknd
## Serve the API
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.
If you don't choose anything specific, it uses the first D1 binding it finds.
```ts title="src/index.ts"
import { serve, d1 } from "bknd/adapter/cloudflare";
@@ -130,46 +129,6 @@ export default serve<Env>({
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 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:
@@ -178,9 +137,6 @@ D1 now supports to enable [global read replication](https://developers.cloudflar
import { serve } from "bknd/adapter/cloudflare";
export default serve({
// currently recommended to use "fresh" mode
// otherwise consecutive requests will use the same bookmark
mode: "fresh",
// ...
d1: {
// enables D1 sessions
@@ -207,7 +163,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:
```ts
import { devFsVitePlugin } from "bknd/adapter/cloudflare/vite";
import { devFsVitePlugin } from "bknd/adapter/cloudflare";
export default defineConfig({
plugins: [devFsVitePlugin()], // [!code highlight]
@@ -217,7 +173,7 @@ export default defineConfig({
Now to use this polyfill, you can use the `devFsWrite` function to write files to the filesystem.
```ts
import { devFsWrite } from "bknd/adapter/cloudflare/vite"; // [!code highlight]
import { devFsWrite } from "bknd/adapter/cloudflare"; // [!code highlight]
import { syncTypes } from "bknd/plugins";
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:
<StackBlitz {...examples.adminRich} />
<StackBlitz path="github/bknd-io/bknd-demo" initialPath="/" />
<Accordions>
<Accordion title="What's going on?">
@@ -224,7 +224,7 @@ Example using `@neondatabase/serverless`:
import { createCustomPostgresConnection } from "@bknd/postgres";
import { NeonDialect } from "kysely-neon";
const neon = createCustomPostgresConnection(NeonDialect);
const neon = createCustomPostgresConnection("neon", NeonDialect);
serve({
connection: neon({
@@ -247,7 +247,7 @@ const xata = new client({
branch: process.env.XATA_BRANCH,
});
const xataConnection = createCustomPostgresConnection(XataDialect, {
const xataConnection = createCustomPostgresConnection("xata", XataDialect, {
supports: {
batching: false,
},
+115 -42
View File
@@ -35,8 +35,7 @@
"minLength": 8
},
"role": {
"type": "string",
"enum": []
"type": "string"
}
},
"required": [
@@ -134,10 +133,17 @@
],
"properties": {
"entity": {
"type": "string",
"enum": [
"users",
"media"
"anyOf": [
{
"type": "string",
"enum": [
"users",
"media"
]
},
{
"type": "string"
}
],
"$target": "param"
},
@@ -161,10 +167,17 @@
],
"properties": {
"entity": {
"type": "string",
"enum": [
"users",
"media"
"anyOf": [
{
"type": "string",
"enum": [
"users",
"media"
]
},
{
"type": "string"
}
],
"$target": "param"
},
@@ -194,10 +207,17 @@
],
"properties": {
"entity": {
"type": "string",
"enum": [
"users",
"media"
"anyOf": [
{
"type": "string",
"enum": [
"users",
"media"
]
},
{
"type": "string"
}
],
"$target": "param"
},
@@ -235,10 +255,17 @@
],
"properties": {
"entity": {
"type": "string",
"enum": [
"users",
"media"
"anyOf": [
{
"type": "string",
"enum": [
"users",
"media"
]
},
{
"type": "string"
}
],
"$target": "param"
},
@@ -276,10 +303,17 @@
],
"properties": {
"entity": {
"type": "string",
"enum": [
"users",
"media"
"anyOf": [
{
"type": "string",
"enum": [
"users",
"media"
]
},
{
"type": "string"
}
],
"$target": "param"
}
@@ -297,10 +331,17 @@
],
"properties": {
"entity": {
"type": "string",
"enum": [
"users",
"media"
"anyOf": [
{
"type": "string",
"enum": [
"users",
"media"
]
},
{
"type": "string"
}
],
"$target": "param"
},
@@ -334,10 +375,17 @@
],
"properties": {
"entity": {
"type": "string",
"enum": [
"users",
"media"
"anyOf": [
{
"type": "string",
"enum": [
"users",
"media"
]
},
{
"type": "string"
}
],
"$target": "param"
},
@@ -410,10 +458,17 @@
],
"properties": {
"entity": {
"type": "string",
"enum": [
"users",
"media"
"anyOf": [
{
"type": "string",
"enum": [
"users",
"media"
]
},
{
"type": "string"
}
],
"$target": "param"
},
@@ -462,10 +517,17 @@
],
"properties": {
"entity": {
"type": "string",
"enum": [
"users",
"media"
"anyOf": [
{
"type": "string",
"enum": [
"users",
"media"
]
},
{
"type": "string"
}
],
"$target": "param"
},
@@ -494,10 +556,17 @@
],
"properties": {
"entity": {
"type": "string",
"enum": [
"users",
"media"
"anyOf": [
{
"type": "string",
"enum": [
"users",
"media"
]
},
{
"type": "string"
}
],
"$target": "param"
},
@@ -3994,6 +4063,10 @@
"enabled": {
"type": "boolean",
"default": false
},
"path": {
"type": "string",
"default": "/api/system/mcp"
}
}
}
+1 -1
View File
@@ -27,7 +27,7 @@ async function generateDocument({
resources: ReturnType<Resource["toJSON"]>[];
}) {
return `---
title: "MCP"
title: "Tools & Resources"
description: "Tools & Resources of the built-in full featured MCP server."
tags: ["documentation"]
---
-14
View File
@@ -1,21 +1,7 @@
import type { CloudflareBkndConfig } from "bknd/adapter/cloudflare";
import { syncTypes } from "bknd/plugins";
import { writeFile } from "node:fs/promises";
const isDev = import.meta.env && !import.meta.env.PROD;
export default {
d1: {
session: true,
},
options: {
plugins: [
syncTypes({
enabled: isDev,
write: async (et) => {
await writeFile("bknd-types.d.ts", et.toString());
},
}),
],
},
} satisfies CloudflareBkndConfig;
+1 -1
View File
@@ -2,7 +2,7 @@
"$schema": "node_modules/wrangler/config-schema.json",
"name": "bknd-cf-worker-example",
"main": "src/index.ts",
"compatibility_date": "2025-02-04",
"compatibility_date": "2025-08-03",
"compatibility_flags": ["nodejs_compat"],
"workers_dev": true,
"minify": true,
+3 -3
View File
@@ -59,7 +59,7 @@ You can create a custom kysely postgres dialect by using the `createCustomPostgr
```ts
import { createCustomPostgresConnection } from "@bknd/postgres";
const connection = createCustomPostgresConnection(MyDialect)({
const connection = createCustomPostgresConnection("my_postgres_dialect", MyDialect)({
// your custom dialect configuration
supports: {
batching: true
@@ -75,7 +75,7 @@ const connection = createCustomPostgresConnection(MyDialect)({
import { createCustomPostgresConnection } from "@bknd/postgres";
import { NeonDialect } from "kysely-neon";
const connection = createCustomPostgresConnection(NeonDialect)({
const connection = createCustomPostgresConnection("neon", NeonDialect)({
connectionString: process.env.NEON,
});
```
@@ -94,7 +94,7 @@ const xata = new client({
branch: process.env.XATA_BRANCH,
});
const connection = createCustomPostgresConnection(XataDialect, {
const connection = createCustomPostgresConnection("xata", XataDialect, {
supports: {
batching: false,
},
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@bknd/postgres",
"version": "0.1.0",
"version": "0.2.0",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
@@ -31,8 +31,7 @@
"@xata.io/kysely": "^0.2.1",
"bknd": "workspace:*",
"kysely-neon": "^1.3.0",
"tsup": "^8.4.0",
"typescript": "^5.8.2"
"tsup": "^8.4.0"
},
"tsup": {
"entry": ["src/index.ts"],
@@ -1,12 +1,13 @@
import { Kysely, PostgresDialect } from "kysely";
import { PostgresIntrospector } from "./PostgresIntrospector";
import { PostgresConnection, plugins } from "./PostgresConnection";
import { customIntrospector } from "bknd/data";
import { customIntrospector } from "bknd";
import $pg from "pg";
export type PgPostgresConnectionConfig = $pg.PoolConfig;
export class PgPostgresConnection extends PostgresConnection {
override name = "pg";
private pool: $pg.Pool;
constructor(config: PgPostgresConnectionConfig) {
+13 -9
View File
@@ -1,4 +1,11 @@
import { Connection, type DbFunctions, type FieldSpec, type SchemaResponse } from "bknd/data";
import {
Connection,
type DbFunctions,
type FieldSpec,
type SchemaResponse,
type ConnQuery,
type ConnQueryResults,
} from "bknd";
import {
ParseJSONResultsPlugin,
type ColumnDataType,
@@ -13,12 +20,13 @@ export type QB = SelectQueryBuilder<any, any, any>;
export const plugins = [new ParseJSONResultsPlugin()];
export abstract class PostgresConnection<DB = any> extends Connection<DB> {
export abstract class PostgresConnection extends Connection {
protected override readonly supported = {
batching: true,
softscans: true,
};
constructor(kysely: Kysely<DB>, fn?: Partial<DbFunctions>, _plugins?: KyselyPlugin[]) {
constructor(kysely: Kysely<any>, fn?: Partial<DbFunctions>, _plugins?: KyselyPlugin[]) {
super(
kysely,
fn ?? {
@@ -73,13 +81,9 @@ export abstract class PostgresConnection<DB = any> extends Connection<DB> {
];
}
protected override async batch<Queries extends QB[]>(
queries: [...Queries],
): Promise<{
[K in keyof Queries]: Awaited<ReturnType<Queries[K]["execute"]>>;
}> {
override async executeQueries<O extends ConnQuery[]>(...qbs: O): Promise<ConnQueryResults<O>> {
return this.kysely.transaction().execute(async (trx) => {
return Promise.all(queries.map((q) => trx.executeQuery(q).then((r) => r.rows)));
return Promise.all(qbs.map((q) => trx.executeQuery(q)));
}) as any;
}
}
@@ -1,5 +1,5 @@
import { type SchemaMetadata, sql } from "kysely";
import { BaseIntrospector } from "bknd/data";
import { BaseIntrospector } from "bknd";
type PostgresSchemaSpec = {
name: string;
@@ -1,13 +1,15 @@
import { Kysely } from "kysely";
import { PostgresIntrospector } from "./PostgresIntrospector";
import { PostgresConnection, plugins } from "./PostgresConnection";
import { customIntrospector } from "bknd/data";
import { customIntrospector } from "bknd";
import { PostgresJSDialect } from "kysely-postgres-js";
import $postgresJs, { type Sql, type Options, type PostgresType } from "postgres";
export type PostgresJsConfig = Options<Record<string, PostgresType>>;
export class PostgresJsConnection extends PostgresConnection {
override name = "postgres-js";
private postgres: Sql;
constructor(opts: { postgres: Sql }) {
+8 -5
View File
@@ -1,9 +1,10 @@
import type { Constructor } from "bknd/core";
import { customIntrospector, type DbFunctions } from "bknd/data";
import { customIntrospector, type DbFunctions } from "bknd";
import { Kysely, type Dialect, type KyselyPlugin } from "kysely";
import { plugins, PostgresConnection } from "./PostgresConnection";
import { PostgresIntrospector } from "./PostgresIntrospector";
export type Constructor<T> = new (...args: any[]) => T;
export type CustomPostgresConnection = {
supports?: PostgresConnection["supported"];
fn?: Partial<DbFunctions>;
@@ -15,17 +16,19 @@ export function createCustomPostgresConnection<
T extends Constructor<Dialect>,
C extends ConstructorParameters<T>[0],
>(
name: string,
dialect: Constructor<Dialect>,
options?: CustomPostgresConnection,
): (config: C) => PostgresConnection<any> {
): (config: C) => PostgresConnection {
const supported = {
batching: true,
...((options?.supports ?? {}) as any),
};
return (config: C) =>
new (class extends PostgresConnection<any> {
protected override readonly supported = supported;
new (class extends PostgresConnection {
override name = name;
override readonly supported = supported;
constructor(config: C) {
super(
+38 -17
View File
@@ -1,8 +1,11 @@
import { describe, beforeAll, afterAll, expect, it, afterEach } from "bun:test";
import type { PostgresConnection } from "../src";
import { createApp } from "bknd";
import * as proto from "bknd/data";
import { createApp, em, entity, text } from "bknd";
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 = {
createConnection: () => InstanceType<typeof PostgresConnection>;
@@ -12,8 +15,9 @@ export type TestSuiteConfig = {
export async function defaultCleanDatabase(connection: InstanceType<typeof PostgresConnection>) {
const kysely = connection.kysely;
// drop all tables & create new schema
// drop all tables+indexes & create new schema
await kysely.schema.dropSchema("public").ifExists().cascade().execute();
await kysely.schema.dropIndex("public").ifExists().cascade().execute();
await kysely.schema.createSchema("public").execute();
}
@@ -32,6 +36,23 @@ export function testSuite(config: TestSuiteConfig) {
beforeAll(() => disableConsoleLog(["log", "warn", "error"]));
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", () => {
it("should connect to the database", async () => {
const connection = config.createConnection();
@@ -73,14 +94,14 @@ export function testSuite(config: TestSuiteConfig) {
});
it("should create a basic schema", async () => {
const schema = proto.em(
const schema = em(
{
posts: proto.entity("posts", {
title: proto.text().required(),
content: proto.text(),
posts: entity("posts", {
title: text().required(),
content: text(),
}),
comments: proto.entity("comments", {
content: proto.text(),
comments: entity("comments", {
content: text(),
}),
},
(fns, s) => {
@@ -153,20 +174,20 @@ export function testSuite(config: TestSuiteConfig) {
});
it("should support uuid", async () => {
const schema = proto.em(
const schema = em(
{
posts: proto.entity(
posts: entity(
"posts",
{
title: proto.text().required(),
content: proto.text(),
title: text().required(),
content: text(),
},
{
primary_format: "uuid",
},
),
comments: proto.entity("comments", {
content: proto.text(),
comments: entity("comments", {
content: text(),
}),
},
(fns, s) => {
@@ -187,8 +208,8 @@ export function testSuite(config: TestSuiteConfig) {
// @ts-expect-error
expect(config.data.entities?.posts.fields?.id.config?.format).toBe("uuid");
const em = app.em;
const mutator = em.mutator(em.entity("posts"));
const $em = app.em;
const mutator = $em.mutator($em.entity("posts"));
const data = await mutator.insertOne({ title: "Hello", content: "World" });
expect(data.data.id).toBeString();
expect(String(data.data.id).length).toBe(36);
+31 -27
View File
@@ -1,29 +1,33 @@
{
"compilerOptions": {
"composite": false,
"module": "ESNext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": false,
"target": "ES2022",
"noImplicitAny": false,
"allowJs": true,
"verbatimModuleSyntax": true,
"declaration": true,
"strict": true,
"allowUnusedLabels": false,
"allowUnreachableCode": false,
"exactOptionalPropertyTypes": false,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noPropertyAccessFromIndexSignature": false,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"isolatedModules": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["./src/**/*.ts"],
"exclude": ["node_modules"]
"compilerOptions": {
"composite": false,
"module": "ESNext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": false,
"target": "ES2022",
"noImplicitAny": false,
"allowJs": true,
"verbatimModuleSyntax": true,
"declaration": true,
"strict": true,
"allowUnusedLabels": false,
"allowUnreachableCode": false,
"exactOptionalPropertyTypes": false,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noPropertyAccessFromIndexSignature": false,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"isolatedModules": true,
"esModuleInterop": true,
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"$bknd/*": ["../../app/src/*"]
}
},
"include": ["./src/**/*.ts"],
"exclude": ["node_modules"]
}