Merge remote-tracking branch 'origin/main' into release/0.18

# Conflicts:
#	app/package.json
This commit is contained in:
dswbx
2025-09-15 16:29:15 +02:00
31 changed files with 340 additions and 176 deletions

View File

@@ -1,10 +1,7 @@
{ {
"mcpServers": { "mcpServers": {
"bknd": { "bknd": {
"url": "http://localhost:3000/mcp", "url": "http://localhost:28623/api/system/mcp"
"headers": {
"API_KEY": "value"
}
} }
} }
} }

View File

@@ -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.

View File

@@ -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",
}), }),

View File

@@ -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...");
} }
} }

View File

@@ -7,6 +7,7 @@ async function generate() {
server: { server: {
mcp: { mcp: {
enabled: true, enabled: true,
path: "/mcp",
}, },
}, },
auth: { auth: {

View File

@@ -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",

View File

@@ -43,7 +43,7 @@ export type ApiOptions = {
} & ( } & (
| { | {
token?: string; token?: string;
user?: TApiUser; user?: TApiUser | null;
} }
| { | {
request: Request; request: Request;

View File

@@ -153,7 +153,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 || [])]);
} }
@@ -161,7 +163,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) {

View File

@@ -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(),
}), }),

View File

@@ -505,3 +505,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;
}

View File

@@ -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;
} }

View File

@@ -1,6 +1,15 @@
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";
@@ -421,7 +430,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);

View File

@@ -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[] = [];

View File

@@ -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);
} }

View File

@@ -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);

View File

@@ -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=="],

View File

@@ -5,7 +5,7 @@ WORKDIR /app
# define bknd version to be used as: # define bknd version to be used as:
# `docker build --build-arg VERSION=<version> -t bknd .` # `docker build --build-arg VERSION=<version> -t bknd .`
ARG VERSION=0.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

View File

@@ -40,24 +40,49 @@ export type BkndConfig = CreateAppConfig & {
onBuilt?: (app: App) => Promise<void>; onBuilt?: (app: App) => Promise<void>;
// passed as the first argument to the `App.build` method // passed as the first argument to the `App.build` method
buildConfig?: Parameters<App["build"]>[0]; buildConfig?: Parameters<App["build"]>[0];
// force the app to be recreated
force?: boolean;
// the id of the app, defaults to `app`
id?: string;
}; };
``` ```
The supported configuration file extensions are `js`, `ts`, `mjs`, `cjs` and `json`. Throughout the documentation, we'll use `ts` for the file extension. The supported configuration file extensions are `js`, `ts`, `mjs`, `cjs` and `json`. Throughout the documentation, we'll use `ts` for the file extension.
## Example
Here is an example of a configuration file that specifies a database connection, registers a plugin, add custom routes using [Hono](https://hono.dev/) and performs a [Kysely](https://kysely.dev/) query.
```typescript
import type { BkndConfig } from "bknd/adapter";
import { showRoutes } from "bknd/plugins";
export default {
connection: {
url: process.env.DB_URL ?? "file:data.db",
},
onBuilt: async (app) => {
// `app.server` is a Hono instance
const hono = app.server;
hono.get("/hello", (c) => c.text("Hello World"));
// for complex queries, you can use Kysely directly
const db = app.connection.kysely;
hono.get("/custom_query", async (c) => {
return c.json(await db.selectFrom("pages").selectAll().execute());
});
},
options: {
plugins: [showRoutes()],
},
} satisfies BkndConfig;
```
### `app` (CreateAppConfig) ### `app` (CreateAppConfig)
The `app` property is a function that returns a `CreateAppConfig` object. It allows to pass in the environment variables to the configuration object. The `app` property is a function that returns a `CreateAppConfig` object. It allows accessing the adapter specific environment variables. This is especially useful when using the [Cloudflare Workers](/integration/cloudflare) runtime, where the environment variables are only available inside the request handler.
```typescript ```typescript
import type { BkndConfig } from "bknd/adapter"; import type { BkndConfig } from "bknd/adapter";
export default { export default {
app: ({ env }) => ({ app: (env) => ({
connection: { connection: {
url: env.DB_URL, url: env.DB_URL,
}, },
@@ -104,12 +129,6 @@ export default {
}; };
``` ```
### `force` & `id`
The `force` property is a boolean that forces the app to be recreated. This is mainly useful for serverless environments where the execution environment is re-used, and you may or may not want to recreate the app on every request.
The `id` property is the reference in a cache map. You may create multiple instances of apps in the same process by using different ids (e.g. multi tenant applications).
## Framework & Runtime configuration ## Framework & Runtime configuration
Depending on which framework or runtime you're using to run bknd, the configuration object will extend the `BkndConfig` type with additional properties. Depending on which framework or runtime you're using to run bknd, the configuration object will extend the `BkndConfig` type with additional properties.

View File

@@ -52,17 +52,17 @@ export default serve();
// manually specifying a D1 binding: // manually specifying a D1 binding:
export default serve<Env>({ export default serve<Env>({
app: ({ env }) => d1({ binding: env.D1_BINDING }), app: (env) => d1({ binding: env.D1_BINDING }),
}); });
// or specify binding using `bindings` // or specify binding using `bindings`
export default serve<Env>({ export default serve<Env>({
bindings: ({ env }) => ({ db: env.D1_BINDING }), bindings: (env) => ({ db: env.D1_BINDING }),
}); });
// or use LibSQL // or use LibSQL
export default serve<Env>({ export default serve<Env>({
app: ({ env }) => ({ url: env.DB_URL }), app: (env) => ({ url: env.DB_URL }),
}); });
``` ```
@@ -81,7 +81,7 @@ your browser.
Now in order to also server the static admin files, you have to modify the `wrangler.toml` to include the static assets. You can do so by either serving the static using the new [Assets feature](https://developers.cloudflare.com/workers/static-assets/), or the deprecated [Workers Site](https://developers.cloudflare.com/workers/configuration/sites/configuration/). Now in order to also server the static admin files, you have to modify the `wrangler.toml` to include the static assets. You can do so by either serving the static using the new [Assets feature](https://developers.cloudflare.com/workers/static-assets/), or the deprecated [Workers Site](https://developers.cloudflare.com/workers/configuration/sites/configuration/).
### Assets ### Assets (recommended)
Make sure your assets point to the static assets included in the bknd package: Make sure your assets point to the static assets included in the bknd package:
@@ -89,7 +89,7 @@ Make sure your assets point to the static assets included in the bknd package:
assets = { directory = "node_modules/bknd/dist/static" } assets = { directory = "node_modules/bknd/dist/static" }
``` ```
### Workers Sites ### Workers Sites (legacy)
Make sure your site points to the static assets included in the bknd package: Make sure your site points to the static assets included in the bknd package:
@@ -108,6 +108,7 @@ export default serve<Env>({
app: () => ({ app: () => ({
/* ... */ /* ... */
}), }),
assets: "kv", // [!code highlight]
manifest, // [!code highlight] manifest, // [!code highlight]
}); });
``` ```
@@ -198,7 +199,7 @@ import { d1 } from "bknd/adapter/cloudflare";
import { withPlatformProxy } from "bknd/adapter/cloudflare/proxy"; import { withPlatformProxy } from "bknd/adapter/cloudflare/proxy";
export default withPlatformProxy({ export default withPlatformProxy({
app: ({ env }) => ({ app: (env) => ({
connection: d1({ binding: env.DB }), connection: d1({ binding: env.DB }),
}), }),
}); });
@@ -216,7 +217,7 @@ Instead, it's recommended to split this configuration into separate files, e.g.
import type { CloudflareBkndConfig } from "bknd/adapter/cloudflare"; import type { CloudflareBkndConfig } from "bknd/adapter/cloudflare";
export default { export default {
app: ({ env }) => ({ app: (env) => ({
connection: d1({ binding: env.DB }), connection: d1({ binding: env.DB }),
}), }),
} satisfies CloudflareBkndConfig; } satisfies CloudflareBkndConfig;

View File

@@ -86,7 +86,7 @@ export default {
url: "file:data.db", url: "file:data.db",
}, },
// or use the `app` function which passes the environment variables // or use the `app` function which passes the environment variables
app: ({ env }) => ({ app: (env) => ({
connection: { connection: {
url: env.DB_URL, url: env.DB_URL,
}, },

View File

@@ -147,7 +147,7 @@ To manually specify which D1 database to take, you can specify it explicitly:
import { serve, d1 } from "bknd/adapter/cloudflare"; import { serve, d1 } from "bknd/adapter/cloudflare";
export default serve<Env>({ export default serve<Env>({
app: ({ env }) => d1({ binding: env.D1_BINDING }), app: (env) => d1({ binding: env.D1_BINDING }),
}); });
``` ```
@@ -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,
}, },

View File

@@ -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"
} }
} }
} }

View File

@@ -59,7 +59,7 @@ You can create a custom kysely postgres dialect by using the `createCustomPostgr
```ts ```ts
import { createCustomPostgresConnection } from "@bknd/postgres"; import { createCustomPostgresConnection } from "@bknd/postgres";
const connection = createCustomPostgresConnection(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,
}, },

View File

@@ -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"],

View File

@@ -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) {

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 { 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;
} }
} }

View File

@@ -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;

View File

@@ -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 }) {

View File

@@ -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(

View File

@@ -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);

View File

@@ -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"]
} }