mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-03 16:46:00 +00:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f8d8d26ac | |||
| 655bb241a8 | |||
| 1ef5bf7d15 | |||
| 9a1589da0b | |||
| f231353228 | |||
| d2de642c17 | |||
| 50c5383138 | |||
| fff88a9237 | |||
| d768a80526 | |||
| 0d79b19c66 | |||
| fea2812688 | |||
| 62368c691a | |||
| dd9617598f | |||
| a7f6d45ca9 | |||
| 92656523ff | |||
| e6ef7d9ff4 | |||
| 210b22e307 | |||
| 734235c798 | |||
| c9773d49a6 | |||
| c9fb497c2d | |||
| ccd4eb10cb | |||
| 847f75b725 | |||
| 8b4bf0d94a | |||
| bf521e2931 | |||
| 111b2ae364 | |||
| dc08d12ce6 | |||
| ce773007f0 | |||
| e78c271c2f |
+1
-4
@@ -1,10 +1,7 @@
|
|||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"bknd": {
|
"bknd": {
|
||||||
"url": "http://localhost:3000/mcp",
|
"url": "http://localhost:28623/api/system/mcp"
|
||||||
"headers": {
|
|
||||||
"API_KEY": "value"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||

|

|
||||||
|
|
||||||
<p align="center" width="100%">
|
<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>
|
<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 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
|
> 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
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
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,6 +52,7 @@ 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:
|
||||||
|
|||||||
+6
-1
@@ -61,8 +61,11 @@ 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",
|
||||||
@@ -86,10 +89,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",
|
||||||
},
|
},
|
||||||
@@ -245,6 +248,8 @@ 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
@@ -17,7 +17,7 @@ async function run(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Read from stdout
|
// Read from stdout
|
||||||
const reader = proc.stdout.getReader();
|
const reader = (proc.stdout as ReadableStream).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.log(c.dim(text.replace(/\n$/, "")));
|
console.info(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.log("adapter", c.cyan(name));
|
console.info("adapter", c.cyan(name));
|
||||||
await config.clean();
|
await config.clean();
|
||||||
|
|
||||||
const { proc, data } = await config.start();
|
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);
|
//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.log("DONE!");
|
console.info("DONE!");
|
||||||
|
|
||||||
while (!proc.killed) {
|
while (!proc.killed) {
|
||||||
proc.kill("SIGINT");
|
proc.kill("SIGINT");
|
||||||
await Bun.sleep(250);
|
await Bun.sleep(250);
|
||||||
console.log("Waiting for process to exit...");
|
console.info("Waiting for process to exit...");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ async function generate() {
|
|||||||
server: {
|
server: {
|
||||||
mcp: {
|
mcp: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
path: "/mcp",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
|
|||||||
+6
-6
@@ -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.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.",
|
"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"
|
"node": ">=22.13"
|
||||||
},
|
},
|
||||||
"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 && 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",
|
"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",
|
||||||
@@ -39,8 +39,8 @@
|
|||||||
"test:adapters": "bun test src/adapter/**/*.adapter.spec.ts --bail",
|
"test:adapters": "bun test src/adapter/**/*.adapter.spec.ts --bail",
|
||||||
"test:coverage": "ALL_TESTS=1 bun test --bail --coverage",
|
"test:coverage": "ALL_TESTS=1 bun test --bail --coverage",
|
||||||
"test:vitest:coverage": "vitest run --coverage",
|
"test:vitest:coverage": "vitest run --coverage",
|
||||||
"test:e2e": "VITE_DB_URL=:memory: playwright test",
|
"test:e2e": "playwright test",
|
||||||
"test:e2e:adapters": "VITE_DB_URL=:memory: 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": "VITE_DB_URL=:memory: playwright test --ui",
|
||||||
"test:e2e:debug": "VITE_DB_URL=:memory: playwright test --debug",
|
"test:e2e:debug": "VITE_DB_URL=:memory: playwright test --debug",
|
||||||
"test:e2e:report": "VITE_DB_URL=:memory: playwright show-report",
|
"test:e2e:report": "VITE_DB_URL=:memory: playwright show-report",
|
||||||
@@ -71,6 +71,7 @@
|
|||||||
"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": {
|
||||||
@@ -108,7 +109,6 @@
|
|||||||
"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
@@ -43,7 +43,7 @@ export type ApiOptions = {
|
|||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
token?: string;
|
token?: string;
|
||||||
user?: TApiUser;
|
user?: TApiUser | null;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
request: Request;
|
request: Request;
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export function adapterTestSuite<
|
|||||||
url: overrides.dbUrl ?? ":memory:",
|
url: overrides.dbUrl ?? ":memory:",
|
||||||
origin: "localhost",
|
origin: "localhost",
|
||||||
} as any,
|
} as any,
|
||||||
{ id },
|
{ force: false, 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, { id });
|
const fetcher = makeHandler(undefined, undefined, { force: false, id });
|
||||||
|
|
||||||
const { res, data } = await getConfig(fetcher);
|
const { res, data } = await getConfig(fetcher);
|
||||||
expect(res.ok).toBe(true);
|
expect(res.ok).toBe(true);
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||||
import { makeApp } from "./modes/fresh";
|
import { makeConfig, type CloudflareContext } from "./config";
|
||||||
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 } from "./cloudflare-workers.adapter";
|
import { type CloudflareBkndConfig, createApp } from "./cloudflare-workers.adapter";
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
beforeAll(disableConsoleLog);
|
||||||
afterAll(enableConsoleLog);
|
afterAll(enableConsoleLog);
|
||||||
@@ -41,18 +40,19 @@ describe("cf adapter", () => {
|
|||||||
expect(dynamicConfig.connection).toBeDefined();
|
expect(dynamicConfig.connection).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
adapterTestSuite<CloudflareBkndConfig, CfMakeConfigArgs<any>>(bunTestRunner, {
|
adapterTestSuite<CloudflareBkndConfig, CloudflareContext<any>>(bunTestRunner, {
|
||||||
makeApp: async (c, a, o) => {
|
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) => {
|
makeHandler: (c, a, o) => {
|
||||||
|
console.log("args", a);
|
||||||
return async (request: any) => {
|
return async (request: any) => {
|
||||||
const app = await makeApp(
|
const app = await createApp(
|
||||||
// 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!,
|
a as any,
|
||||||
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 { getFresh } from "./modes/fresh";
|
import type { MaybePromise } from "bknd";
|
||||||
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,7 +16,6 @@ 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;
|
||||||
@@ -34,11 +33,31 @@ export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> &
|
|||||||
registerMedia?: boolean | ((env: Env) => void);
|
registerMedia?: boolean | ((env: Env) => void);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Context<Env = CloudflareEnv> = {
|
export async function createApp<Env extends CloudflareEnv = CloudflareEnv>(
|
||||||
request: Request;
|
config: CloudflareBkndConfig<Env>,
|
||||||
env: Env;
|
ctx: Partial<CloudflareContext<Env>> = {},
|
||||||
ctx: ExecutionContext;
|
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>(
|
export function serve<Env extends CloudflareEnv = CloudflareEnv>(
|
||||||
config: CloudflareBkndConfig<Env> = {},
|
config: CloudflareBkndConfig<Env> = {},
|
||||||
@@ -77,23 +96,8 @@ export function serve<Env extends CloudflareEnv = CloudflareEnv>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const context = { request, env, ctx } as Context<Env>;
|
const context = { request, env, ctx } as CloudflareContext<Env>;
|
||||||
const mode = config.mode ?? "warm";
|
const app = await createApp(config, context);
|
||||||
|
|
||||||
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);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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, ExecutionContext } from "hono";
|
import type { Context as HonoContext, 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 CfMakeConfigArgs<Env extends CloudflareEnv = CloudflareEnv> = {
|
export type CloudflareContext<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: Context, d1?: D1DatabaseSession) => {
|
set: (c: HonoContext, 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?: CfMakeConfigArgs<Env>,
|
args?: Partial<CloudflareContext<Env>>,
|
||||||
) {
|
) {
|
||||||
if (!media_registered && config.registerMedia !== false) {
|
if (!media_registered && config.registerMedia !== false) {
|
||||||
if (typeof config.registerMedia === "function") {
|
if (typeof config.registerMedia === "function") {
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { d1Sqlite, type D1ConnectionConfig } from "./connection/D1Connection";
|
import { d1Sqlite, type D1ConnectionConfig } from "./connection/D1Connection";
|
||||||
|
|
||||||
export * from "./cloudflare-workers.adapter";
|
export {
|
||||||
export { makeApp, getFresh } from "./modes/fresh";
|
getFresh,
|
||||||
export { getCached } from "./modes/cached";
|
createApp,
|
||||||
|
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 {
|
||||||
@@ -12,7 +16,7 @@ export {
|
|||||||
type GetBindingType,
|
type GetBindingType,
|
||||||
type BindingMap,
|
type BindingMap,
|
||||||
} from "./bindings";
|
} from "./bindings";
|
||||||
export { constants } from "./config";
|
export { constants, type CloudflareContext } 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 { 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;
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,7 @@ export function devFsVitePlugin({
|
|||||||
}: {
|
}: {
|
||||||
verbose?: boolean;
|
verbose?: boolean;
|
||||||
configFile?: string;
|
configFile?: string;
|
||||||
}): Plugin {
|
} = {}): any {
|
||||||
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
|
||||||
|
|||||||
@@ -83,8 +83,12 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +173,9 @@ 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 || ((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 || [])]);
|
files = Object.values(manifest).flatMap((asset) => [asset.file, ...(asset.css || [])]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +183,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`, {
|
||||||
assert: { type: "text" },
|
with: { type: "text" },
|
||||||
}).then((m) => m.default);
|
}).then((m) => m.default);
|
||||||
|
|
||||||
if (content) {
|
if (content) {
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ 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",
|
||||||
@@ -231,7 +232,7 @@ export class AuthController extends Controller {
|
|||||||
password: s.string({ minLength: 8 }),
|
password: s.string({ minLength: 8 }),
|
||||||
role: s
|
role: s
|
||||||
.string({
|
.string({
|
||||||
enum: Object.keys(this.auth.config.roles ?? {}),
|
enum: roles.length > 0 ? roles : undefined,
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -473,3 +473,10 @@ 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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import { v4, v7 } from "uuid";
|
import { v4, v7, validate, version as uuidVersion } 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
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 { 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 * 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";
|
||||||
@@ -37,14 +45,6 @@ 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,10 +52,7 @@ export class DataController extends Controller {
|
|||||||
summary: "Retrieve data configuration",
|
summary: "Retrieve data configuration",
|
||||||
tags: ["data"],
|
tags: ["data"],
|
||||||
}),
|
}),
|
||||||
handler("data info", (c) => {
|
(c) => c.json(this.em.toJSON()),
|
||||||
// sample implementation
|
|
||||||
return c.json(this.em.toJSON());
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// sync endpoint
|
// sync endpoint
|
||||||
@@ -432,7 +429,13 @@ 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);
|
||||||
|
|||||||
@@ -258,6 +258,9 @@ 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[] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -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.string({ enum: entities }) : s.string();
|
return entities.length > 0 ? s.anyOf([s.string({ enum: entities }), s.string()]) : s.string();
|
||||||
}
|
}
|
||||||
|
|
||||||
registerMcp(): void {}
|
registerMcp(): void {}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ 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 -->";
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ 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 {
|
||||||
@@ -194,8 +196,10 @@ export class AdminController extends Controller {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (isProd) {
|
if (isProd) {
|
||||||
let manifest: any;
|
let manifest: Manifest;
|
||||||
if (this.options.assetsPath.startsWith("http")) {
|
if (this.options.manifest) {
|
||||||
|
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",
|
||||||
@@ -204,14 +208,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", {
|
||||||
assert: { type: "json" },
|
with: { 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -376,6 +376,7 @@ 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 }),
|
||||||
});
|
});
|
||||||
type ImageOptimizationSchema = s.Static<typeof schema>;
|
export type CloudflareImageOptimizationSchema = 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?: ImageOptimizationSchema;
|
defaultOptions?: CloudflareImageOptimizationSchema;
|
||||||
/**
|
/**
|
||||||
* The fixed options to use
|
* The fixed options to use
|
||||||
* @default {}
|
* @default {}
|
||||||
*/
|
*/
|
||||||
fixedOptions?: ImageOptimizationSchema;
|
fixedOptions?: CloudflareImageOptimizationSchema;
|
||||||
/**
|
/**
|
||||||
* The cache control to use
|
* The cache control to use
|
||||||
* @default public, max-age=31536000, immutable
|
* @default public, max-age=31536000, immutable
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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";
|
||||||
|
|||||||
@@ -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", resize);
|
||||||
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}>
|
||||||
|
|||||||
@@ -122,3 +122,7 @@ 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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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="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) => (
|
{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">
|
<Formy.Label as="legend" className="font-mono px-2 w-min-content">
|
||||||
{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}>
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ html.fixed,
|
|||||||
html.fixed body {
|
html.fixed body {
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
height: 100%;
|
right: 0;
|
||||||
width: 100%;
|
bottom: 0;
|
||||||
|
height: 100dvh;
|
||||||
|
width: 100dvw;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
overscroll-behavior-x: contain;
|
overscroll-behavior-x: contain;
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ 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);
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"name": "bknd",
|
"name": "bknd",
|
||||||
"version": "0.17.0-rc.1",
|
"version": "0.17.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",
|
||||||
@@ -151,7 +151,6 @@
|
|||||||
"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",
|
||||||
@@ -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/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=="],
|
"@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/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=="],
|
||||||
@@ -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=="],
|
"@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=="],
|
"@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=="],
|
"@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
@@ -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.13.0
|
ARG VERSION=0.17.1
|
||||||
|
|
||||||
# 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 pm2 and libsql
|
# Install required dependencies
|
||||||
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 @libsql/client
|
RUN npm install jsonv-ts @libsql/client
|
||||||
|
|
||||||
# Create volume and init args
|
# Create volume and init args
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
|
|||||||
@@ -3,79 +3,77 @@
|
|||||||
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(
|
const url = new URL(`https://stackblitz.com/${finalPath}?${params.toString()}`);
|
||||||
`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’re having issues viewing it inline,{" "}
|
If you’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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -42,8 +42,7 @@ bun add bknd
|
|||||||
|
|
||||||
## Serve the API
|
## 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
|
If you don't choose anything specific, it uses the first D1 binding it finds.
|
||||||
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";
|
||||||
@@ -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.
|
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:
|
||||||
@@ -178,9 +137,6 @@ 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
|
||||||
@@ -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:
|
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/vite";
|
import { devFsVitePlugin } from "bknd/adapter/cloudflare";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [devFsVitePlugin()], // [!code highlight]
|
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.
|
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/vite"; // [!code highlight]
|
import { devFsWrite } from "bknd/adapter/cloudflare"; // [!code highlight]
|
||||||
import { syncTypes } from "bknd/plugins";
|
import { syncTypes } from "bknd/plugins";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -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 {...examples.adminRich} />
|
<StackBlitz path="github/bknd-io/bknd-demo" initialPath="/" />
|
||||||
|
|
||||||
<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(NeonDialect);
|
const neon = createCustomPostgresConnection("neon", 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(XataDialect, {
|
const xataConnection = createCustomPostgresConnection("xata", XataDialect, {
|
||||||
supports: {
|
supports: {
|
||||||
batching: false,
|
batching: false,
|
||||||
},
|
},
|
||||||
|
|||||||
+115
-42
@@ -35,8 +35,7 @@
|
|||||||
"minLength": 8
|
"minLength": 8
|
||||||
},
|
},
|
||||||
"role": {
|
"role": {
|
||||||
"type": "string",
|
"type": "string"
|
||||||
"enum": []
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -134,10 +133,17 @@
|
|||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"entity": {
|
"entity": {
|
||||||
"type": "string",
|
"anyOf": [
|
||||||
"enum": [
|
{
|
||||||
"users",
|
"type": "string",
|
||||||
"media"
|
"enum": [
|
||||||
|
"users",
|
||||||
|
"media"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"$target": "param"
|
"$target": "param"
|
||||||
},
|
},
|
||||||
@@ -161,10 +167,17 @@
|
|||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"entity": {
|
"entity": {
|
||||||
"type": "string",
|
"anyOf": [
|
||||||
"enum": [
|
{
|
||||||
"users",
|
"type": "string",
|
||||||
"media"
|
"enum": [
|
||||||
|
"users",
|
||||||
|
"media"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"$target": "param"
|
"$target": "param"
|
||||||
},
|
},
|
||||||
@@ -194,10 +207,17 @@
|
|||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"entity": {
|
"entity": {
|
||||||
"type": "string",
|
"anyOf": [
|
||||||
"enum": [
|
{
|
||||||
"users",
|
"type": "string",
|
||||||
"media"
|
"enum": [
|
||||||
|
"users",
|
||||||
|
"media"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"$target": "param"
|
"$target": "param"
|
||||||
},
|
},
|
||||||
@@ -235,10 +255,17 @@
|
|||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"entity": {
|
"entity": {
|
||||||
"type": "string",
|
"anyOf": [
|
||||||
"enum": [
|
{
|
||||||
"users",
|
"type": "string",
|
||||||
"media"
|
"enum": [
|
||||||
|
"users",
|
||||||
|
"media"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"$target": "param"
|
"$target": "param"
|
||||||
},
|
},
|
||||||
@@ -276,10 +303,17 @@
|
|||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"entity": {
|
"entity": {
|
||||||
"type": "string",
|
"anyOf": [
|
||||||
"enum": [
|
{
|
||||||
"users",
|
"type": "string",
|
||||||
"media"
|
"enum": [
|
||||||
|
"users",
|
||||||
|
"media"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"$target": "param"
|
"$target": "param"
|
||||||
}
|
}
|
||||||
@@ -297,10 +331,17 @@
|
|||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"entity": {
|
"entity": {
|
||||||
"type": "string",
|
"anyOf": [
|
||||||
"enum": [
|
{
|
||||||
"users",
|
"type": "string",
|
||||||
"media"
|
"enum": [
|
||||||
|
"users",
|
||||||
|
"media"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"$target": "param"
|
"$target": "param"
|
||||||
},
|
},
|
||||||
@@ -334,10 +375,17 @@
|
|||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"entity": {
|
"entity": {
|
||||||
"type": "string",
|
"anyOf": [
|
||||||
"enum": [
|
{
|
||||||
"users",
|
"type": "string",
|
||||||
"media"
|
"enum": [
|
||||||
|
"users",
|
||||||
|
"media"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"$target": "param"
|
"$target": "param"
|
||||||
},
|
},
|
||||||
@@ -410,10 +458,17 @@
|
|||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"entity": {
|
"entity": {
|
||||||
"type": "string",
|
"anyOf": [
|
||||||
"enum": [
|
{
|
||||||
"users",
|
"type": "string",
|
||||||
"media"
|
"enum": [
|
||||||
|
"users",
|
||||||
|
"media"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"$target": "param"
|
"$target": "param"
|
||||||
},
|
},
|
||||||
@@ -462,10 +517,17 @@
|
|||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"entity": {
|
"entity": {
|
||||||
"type": "string",
|
"anyOf": [
|
||||||
"enum": [
|
{
|
||||||
"users",
|
"type": "string",
|
||||||
"media"
|
"enum": [
|
||||||
|
"users",
|
||||||
|
"media"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"$target": "param"
|
"$target": "param"
|
||||||
},
|
},
|
||||||
@@ -494,10 +556,17 @@
|
|||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"entity": {
|
"entity": {
|
||||||
"type": "string",
|
"anyOf": [
|
||||||
"enum": [
|
{
|
||||||
"users",
|
"type": "string",
|
||||||
"media"
|
"enum": [
|
||||||
|
"users",
|
||||||
|
"media"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"$target": "param"
|
"$target": "param"
|
||||||
},
|
},
|
||||||
@@ -3994,6 +4063,10 @@
|
|||||||
"enabled": {
|
"enabled": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": false
|
"default": false
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "/api/system/mcp"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ async function generateDocument({
|
|||||||
resources: ReturnType<Resource["toJSON"]>[];
|
resources: ReturnType<Resource["toJSON"]>[];
|
||||||
}) {
|
}) {
|
||||||
return `---
|
return `---
|
||||||
title: "MCP"
|
title: "Tools & Resources"
|
||||||
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,21 +1,7 @@
|
|||||||
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 && !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;
|
||||||
|
|||||||
@@ -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-02-04",
|
"compatibility_date": "2025-08-03",
|
||||||
"compatibility_flags": ["nodejs_compat"],
|
"compatibility_flags": ["nodejs_compat"],
|
||||||
"workers_dev": true,
|
"workers_dev": true,
|
||||||
"minify": true,
|
"minify": true,
|
||||||
|
|||||||
@@ -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(MyDialect)({
|
const connection = createCustomPostgresConnection("my_postgres_dialect", MyDialect)({
|
||||||
// your custom dialect configuration
|
// your custom dialect configuration
|
||||||
supports: {
|
supports: {
|
||||||
batching: true
|
batching: true
|
||||||
@@ -75,7 +75,7 @@ const connection = createCustomPostgresConnection(MyDialect)({
|
|||||||
import { createCustomPostgresConnection } from "@bknd/postgres";
|
import { createCustomPostgresConnection } from "@bknd/postgres";
|
||||||
import { NeonDialect } from "kysely-neon";
|
import { NeonDialect } from "kysely-neon";
|
||||||
|
|
||||||
const connection = createCustomPostgresConnection(NeonDialect)({
|
const connection = createCustomPostgresConnection("neon", 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(XataDialect, {
|
const connection = createCustomPostgresConnection("xata", XataDialect, {
|
||||||
supports: {
|
supports: {
|
||||||
batching: false,
|
batching: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@bknd/postgres",
|
"name": "@bknd/postgres",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.js",
|
"module": "dist/index.js",
|
||||||
@@ -31,8 +31,7 @@
|
|||||||
"@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,12 +1,13 @@
|
|||||||
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/data";
|
import { customIntrospector } from "bknd";
|
||||||
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) {
|
||||||
|
|||||||
@@ -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 {
|
import {
|
||||||
ParseJSONResultsPlugin,
|
ParseJSONResultsPlugin,
|
||||||
type ColumnDataType,
|
type ColumnDataType,
|
||||||
@@ -13,12 +20,13 @@ export type QB = SelectQueryBuilder<any, any, any>;
|
|||||||
|
|
||||||
export const plugins = [new ParseJSONResultsPlugin()];
|
export const plugins = [new ParseJSONResultsPlugin()];
|
||||||
|
|
||||||
export abstract class PostgresConnection<DB = any> extends Connection<DB> {
|
export abstract class PostgresConnection extends Connection {
|
||||||
protected override readonly supported = {
|
protected override readonly supported = {
|
||||||
batching: true,
|
batching: true,
|
||||||
|
softscans: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(kysely: Kysely<DB>, fn?: Partial<DbFunctions>, _plugins?: KyselyPlugin[]) {
|
constructor(kysely: Kysely<any>, fn?: Partial<DbFunctions>, _plugins?: KyselyPlugin[]) {
|
||||||
super(
|
super(
|
||||||
kysely,
|
kysely,
|
||||||
fn ?? {
|
fn ?? {
|
||||||
@@ -73,13 +81,9 @@ export abstract class PostgresConnection<DB = any> extends Connection<DB> {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async batch<Queries extends QB[]>(
|
override async executeQueries<O extends ConnQuery[]>(...qbs: O): Promise<ConnQueryResults<O>> {
|
||||||
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(queries.map((q) => trx.executeQuery(q).then((r) => r.rows)));
|
return Promise.all(qbs.map((q) => trx.executeQuery(q)));
|
||||||
}) as any;
|
}) as any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { type SchemaMetadata, sql } from "kysely";
|
import { type SchemaMetadata, sql } from "kysely";
|
||||||
import { BaseIntrospector } from "bknd/data";
|
import { BaseIntrospector } from "bknd";
|
||||||
|
|
||||||
type PostgresSchemaSpec = {
|
type PostgresSchemaSpec = {
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
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/data";
|
import { customIntrospector } from "bknd";
|
||||||
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 }) {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import type { Constructor } from "bknd/core";
|
import { customIntrospector, type DbFunctions } from "bknd";
|
||||||
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>;
|
||||||
@@ -15,17 +16,19 @@ 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<any> {
|
): (config: C) => PostgresConnection {
|
||||||
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<any> {
|
new (class extends PostgresConnection {
|
||||||
protected override readonly supported = supported;
|
override name = name;
|
||||||
|
override readonly supported = supported;
|
||||||
|
|
||||||
constructor(config: C) {
|
constructor(config: C) {
|
||||||
super(
|
super(
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
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 } from "bknd";
|
import { createApp, em, entity, text } 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>;
|
||||||
@@ -12,8 +15,9 @@ 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 & create new schema
|
// drop all tables+indexes & 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +36,23 @@ 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();
|
||||||
@@ -73,14 +94,14 @@ export function testSuite(config: TestSuiteConfig) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should create a basic schema", async () => {
|
it("should create a basic schema", async () => {
|
||||||
const schema = proto.em(
|
const schema = em(
|
||||||
{
|
{
|
||||||
posts: proto.entity("posts", {
|
posts: entity("posts", {
|
||||||
title: proto.text().required(),
|
title: text().required(),
|
||||||
content: proto.text(),
|
content: text(),
|
||||||
}),
|
}),
|
||||||
comments: proto.entity("comments", {
|
comments: entity("comments", {
|
||||||
content: proto.text(),
|
content: text(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
(fns, s) => {
|
(fns, s) => {
|
||||||
@@ -153,20 +174,20 @@ export function testSuite(config: TestSuiteConfig) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should support uuid", async () => {
|
it("should support uuid", async () => {
|
||||||
const schema = proto.em(
|
const schema = em(
|
||||||
{
|
{
|
||||||
posts: proto.entity(
|
posts: entity(
|
||||||
"posts",
|
"posts",
|
||||||
{
|
{
|
||||||
title: proto.text().required(),
|
title: text().required(),
|
||||||
content: proto.text(),
|
content: text(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
primary_format: "uuid",
|
primary_format: "uuid",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
comments: proto.entity("comments", {
|
comments: entity("comments", {
|
||||||
content: proto.text(),
|
content: text(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
(fns, s) => {
|
(fns, s) => {
|
||||||
@@ -187,8 +208,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);
|
||||||
|
|||||||
@@ -1,29 +1,33 @@
|
|||||||
{
|
{
|
||||||
"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": ".",
|
||||||
"include": ["./src/**/*.ts"],
|
"paths": {
|
||||||
"exclude": ["node_modules"]
|
"$bknd/*": ["../../app/src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["./src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user