From a7f6d45ca92d9d4bf3a3fb9286aa1a3c7b3522b4 Mon Sep 17 00:00:00 2001 From: dswbx Date: Sun, 14 Sep 2025 13:43:45 +0200 Subject: [PATCH 1/9] fix raw node execution by making sure import attributes stick on builds --- README.md | 2 +- app/build.ts | 6 ++++-- app/package.json | 2 +- app/src/adapter/index.ts | 6 ++++-- app/src/modules/server/AdminController.tsx | 14 +++++++++----- bun.lock | 6 +++--- 6 files changed, 22 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index ab56a80..8761a0e 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ bknd simplifies app development by providing a fully functional backend for data **For documentation and examples, please visit https://docs.bknd.io.** > [!WARNING] -> This project requires Node.js 22 or higher (because of `node:sqlite`). +> This project requires Node.js 22.13 or higher (because of `node:sqlite`). > > Please keep in mind that **bknd** is still under active development > and therefore full backward compatibility is not guaranteed before reaching v1.0.0. diff --git a/app/build.ts b/app/build.ts index 2d0caa9..eb15450 100644 --- a/app/build.ts +++ b/app/build.ts @@ -86,10 +86,10 @@ async function buildApi() { outDir: "dist", external: [...external], metafile: true, - platform: "browser", + target: "esnext", + platform: "neutral", format: ["esm"], splitting: false, - treeshake: true, loader: { ".svg": "dataurl", }, @@ -245,6 +245,8 @@ async function buildAdapters() { // base adapter handles tsup.build({ ...baseConfig(""), + target: "esnext", + platform: "neutral", entry: ["src/adapter/index.ts"], outDir: "dist/adapter", }), diff --git a/app/package.json b/app/package.json index c398fa5..aea0395 100644 --- a/app/package.json +++ b/app/package.json @@ -15,7 +15,7 @@ }, "packageManager": "bun@1.2.19", "engines": { - "node": ">=22" + "node": ">=22.13" }, "scripts": { "dev": "BKND_CLI_LOG_LEVEL=debug vite", diff --git a/app/src/adapter/index.ts b/app/src/adapter/index.ts index 91ffcf7..17827f7 100644 --- a/app/src/adapter/index.ts +++ b/app/src/adapter/index.ts @@ -173,7 +173,9 @@ export function serveStaticViaImport(opts?: { manifest?: Manifest }) { return async (c: Context, next: Next) => { if (!files) { const manifest = - opts?.manifest || ((await import("bknd/dist/manifest.json")).default as Manifest); + opts?.manifest || + ((await import("bknd/dist/manifest.json", { with: { type: "json" } })) + .default as Manifest); files = Object.values(manifest).flatMap((asset) => [asset.file, ...(asset.css || [])]); } @@ -181,7 +183,7 @@ export function serveStaticViaImport(opts?: { manifest?: Manifest }) { if (files.includes(path)) { try { const content = await import(/* @vite-ignore */ `bknd/static/${path}?raw`, { - assert: { type: "text" }, + with: { type: "text" }, }).then((m) => m.default); if (content) { diff --git a/app/src/modules/server/AdminController.tsx b/app/src/modules/server/AdminController.tsx index d15eefe..a68519d 100644 --- a/app/src/modules/server/AdminController.tsx +++ b/app/src/modules/server/AdminController.tsx @@ -11,6 +11,7 @@ import { css, Style } from "hono/css"; import { Controller } from "modules/Controller"; import * as SystemPermissions from "modules/permissions"; import type { TApiUser } from "Api"; +import type { Manifest } from "vite"; const htmlBkndContextReplace = ""; @@ -32,6 +33,7 @@ export type AdminControllerOptions = { debugRerenders?: boolean; theme?: "dark" | "light" | "system"; logoReturnPath?: string; + manifest?: Manifest; }; export class AdminController extends Controller { @@ -194,8 +196,10 @@ export class AdminController extends Controller { }; if (isProd) { - let manifest: any; - if (this.options.assetsPath.startsWith("http")) { + let manifest: Manifest; + if (this.options.manifest) { + manifest = this.options.manifest; + } else if (this.options.assetsPath.startsWith("http")) { manifest = await fetch(this.options.assetsPath + ".vite/manifest.json", { headers: { Accept: "application/json", @@ -204,14 +208,14 @@ export class AdminController extends Controller { } else { // @ts-ignore manifest = await import("bknd/dist/manifest.json", { - assert: { type: "json" }, + with: { type: "json" }, }).then((res) => res.default); } try { // @todo: load all marked as entry (incl. css) - assets.js = manifest["src/ui/main.tsx"].file; - assets.css = manifest["src/ui/main.tsx"].css[0] as any; + assets.js = manifest["src/ui/main.tsx"]?.file!; + assets.css = manifest["src/ui/main.tsx"]?.css?.[0] as any; } catch (e) { $console.warn("Couldn't find assets in manifest", e); } diff --git a/bun.lock b/bun.lock index d9765f6..c7350b2 100644 --- a/bun.lock +++ b/bun.lock @@ -15,7 +15,7 @@ }, "app": { "name": "bknd", - "version": "0.17.0-rc.1", + "version": "0.17.1", "bin": "./dist/cli/index.js", "dependencies": { "@cfworker/json-schema": "^4.1.1", @@ -1232,7 +1232,7 @@ "@types/babel__traverse": ["@types/babel__traverse@7.20.6", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg=="], - "@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="], + "@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="], "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], @@ -4078,7 +4078,7 @@ "@testing-library/jest-dom/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], - "@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=="], From dd9617598f52b94fb0b7986cdb67aaac91ba2ba7 Mon Sep 17 00:00:00 2001 From: dswbx Date: Sun, 14 Sep 2025 14:07:56 +0200 Subject: [PATCH 2/9] bump Dockerfile to use bknd 0.17.1 Updated the default bknd version to 0.17.1 and included the `jsonv-ts` dependency in the Dockerfile. --- docker/Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 132289a..c946b6a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -5,7 +5,7 @@ WORKDIR /app # define bknd version to be used as: # `docker build --build-arg VERSION= -t bknd .` -ARG VERSION=0.13.0 +ARG VERSION=0.17.1 # Install & copy required cli RUN npm install --omit=dev bknd@${VERSION} @@ -16,10 +16,10 @@ FROM node:24-alpine WORKDIR /app -# Install pm2 and libsql +# Install required dependencies RUN npm install -g pm2 RUN echo '{"type":"module"}' > package.json -RUN npm install @libsql/client +RUN npm install jsonv-ts @libsql/client # Create volume and init args VOLUME /data From 62368c691a22a7734842acba9ebcf60e8bb8b113 Mon Sep 17 00:00:00 2001 From: dswbx Date: Sun, 14 Sep 2025 16:01:37 +0200 Subject: [PATCH 3/9] postgres: bump 0.17.1 and improve custom connection API Aligned connection constructors to include an explicit name parameter, updated documentation, and streamlined connection methods for consistency. Adjusted dependencies and cleaned unused references. --- app/src/data/entities/EntityManager.ts | 3 + bun.lock | 13 +---- .../docs/(documentation)/usage/database.mdx | 4 +- packages/postgres/README.md | 6 +- packages/postgres/package.json | 3 +- packages/postgres/src/PgPostgresConnection.ts | 3 +- packages/postgres/src/PostgresConnection.ts | 22 ++++--- packages/postgres/src/PostgresIntrospector.ts | 2 +- packages/postgres/src/PostgresJsConnection.ts | 4 +- packages/postgres/src/custom.ts | 13 +++-- packages/postgres/test/suite.ts | 55 ++++++++++++------ packages/postgres/tsconfig.json | 58 ++++++++++--------- 12 files changed, 108 insertions(+), 78 deletions(-) diff --git a/app/src/data/entities/EntityManager.ts b/app/src/data/entities/EntityManager.ts index 544c8ad..36168f8 100644 --- a/app/src/data/entities/EntityManager.ts +++ b/app/src/data/entities/EntityManager.ts @@ -258,6 +258,9 @@ export class EntityManager { // @todo: centralize and add tests hydrate(entity_name: string, _data: EntityData[]) { + if (!Array.isArray(_data) || _data.length === 0) { + return []; + } const entity = this.entity(entity_name); const data: EntityData[] = []; diff --git a/bun.lock b/bun.lock index d9765f6..9e26bd2 100644 --- a/bun.lock +++ b/bun.lock @@ -15,7 +15,7 @@ }, "app": { "name": "bknd", - "version": "0.17.0-rc.1", + "version": "0.17.1", "bin": "./dist/cli/index.js", "dependencies": { "@cfworker/json-schema": "^4.1.1", @@ -151,7 +151,6 @@ "bknd": "workspace:*", "kysely-neon": "^1.3.0", "tsup": "^8.4.0", - "typescript": "^5.8.2", }, "optionalDependencies": { "kysely": "^0.27.6", @@ -1232,7 +1231,7 @@ "@types/babel__traverse": ["@types/babel__traverse@7.20.6", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg=="], - "@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="], + "@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="], "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], @@ -3832,10 +3831,6 @@ "@bknd/plasmic/typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], - "@bknd/postgres/@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="], - - "@bknd/postgres/typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], - "@bknd/sqlocal/typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], "@bundled-es-modules/cookie/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], @@ -4078,7 +4073,7 @@ "@testing-library/jest-dom/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], - "@types/bun/bun-types": ["bun-types@1.2.20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="], + "@types/bun/bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="], "@typescript-eslint/experimental-utils/eslint-utils": ["eslint-utils@2.1.0", "", { "dependencies": { "eslint-visitor-keys": "^1.1.0" } }, "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg=="], @@ -4684,8 +4679,6 @@ "@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.3", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg=="], - "@bknd/postgres/@types/bun/bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="], - "@bundled-es-modules/tough-cookie/tough-cookie/universalify": ["universalify@0.2.0", "", {}, "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="], "@cloudflare/vitest-pool-workers/miniflare/sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], diff --git a/docs/content/docs/(documentation)/usage/database.mdx b/docs/content/docs/(documentation)/usage/database.mdx index febffdd..b09587f 100644 --- a/docs/content/docs/(documentation)/usage/database.mdx +++ b/docs/content/docs/(documentation)/usage/database.mdx @@ -224,7 +224,7 @@ Example using `@neondatabase/serverless`: import { createCustomPostgresConnection } from "@bknd/postgres"; import { NeonDialect } from "kysely-neon"; -const neon = createCustomPostgresConnection(NeonDialect); +const neon = createCustomPostgresConnection("neon", NeonDialect); serve({ connection: neon({ @@ -247,7 +247,7 @@ const xata = new client({ branch: process.env.XATA_BRANCH, }); -const xataConnection = createCustomPostgresConnection(XataDialect, { +const xataConnection = createCustomPostgresConnection("xata", XataDialect, { supports: { batching: false, }, diff --git a/packages/postgres/README.md b/packages/postgres/README.md index b66395b..ed5d1e6 100644 --- a/packages/postgres/README.md +++ b/packages/postgres/README.md @@ -59,7 +59,7 @@ You can create a custom kysely postgres dialect by using the `createCustomPostgr ```ts import { createCustomPostgresConnection } from "@bknd/postgres"; -const connection = createCustomPostgresConnection(MyDialect)({ +const connection = createCustomPostgresConnection("my_postgres_dialect", MyDialect)({ // your custom dialect configuration supports: { batching: true @@ -75,7 +75,7 @@ const connection = createCustomPostgresConnection(MyDialect)({ import { createCustomPostgresConnection } from "@bknd/postgres"; import { NeonDialect } from "kysely-neon"; -const connection = createCustomPostgresConnection(NeonDialect)({ +const connection = createCustomPostgresConnection("neon", NeonDialect)({ connectionString: process.env.NEON, }); ``` @@ -94,7 +94,7 @@ const xata = new client({ branch: process.env.XATA_BRANCH, }); -const connection = createCustomPostgresConnection(XataDialect, { +const connection = createCustomPostgresConnection("xata", XataDialect, { supports: { batching: false, }, diff --git a/packages/postgres/package.json b/packages/postgres/package.json index 6d77c10..e822af4 100644 --- a/packages/postgres/package.json +++ b/packages/postgres/package.json @@ -31,8 +31,7 @@ "@xata.io/kysely": "^0.2.1", "bknd": "workspace:*", "kysely-neon": "^1.3.0", - "tsup": "^8.4.0", - "typescript": "^5.8.2" + "tsup": "^8.4.0" }, "tsup": { "entry": ["src/index.ts"], diff --git a/packages/postgres/src/PgPostgresConnection.ts b/packages/postgres/src/PgPostgresConnection.ts index 85a5c84..c96e693 100644 --- a/packages/postgres/src/PgPostgresConnection.ts +++ b/packages/postgres/src/PgPostgresConnection.ts @@ -1,12 +1,13 @@ import { Kysely, PostgresDialect } from "kysely"; import { PostgresIntrospector } from "./PostgresIntrospector"; import { PostgresConnection, plugins } from "./PostgresConnection"; -import { customIntrospector } from "bknd/data"; +import { customIntrospector } from "bknd"; import $pg from "pg"; export type PgPostgresConnectionConfig = $pg.PoolConfig; export class PgPostgresConnection extends PostgresConnection { + override name = "pg"; private pool: $pg.Pool; constructor(config: PgPostgresConnectionConfig) { diff --git a/packages/postgres/src/PostgresConnection.ts b/packages/postgres/src/PostgresConnection.ts index 9ea1d2c..ff67991 100644 --- a/packages/postgres/src/PostgresConnection.ts +++ b/packages/postgres/src/PostgresConnection.ts @@ -1,4 +1,11 @@ -import { Connection, type DbFunctions, type FieldSpec, type SchemaResponse } from "bknd/data"; +import { + Connection, + type DbFunctions, + type FieldSpec, + type SchemaResponse, + type ConnQuery, + type ConnQueryResults, +} from "bknd"; import { ParseJSONResultsPlugin, type ColumnDataType, @@ -13,12 +20,13 @@ export type QB = SelectQueryBuilder; export const plugins = [new ParseJSONResultsPlugin()]; -export abstract class PostgresConnection extends Connection { +export abstract class PostgresConnection extends Connection { protected override readonly supported = { batching: true, + softscans: true, }; - constructor(kysely: Kysely, fn?: Partial, _plugins?: KyselyPlugin[]) { + constructor(kysely: Kysely, fn?: Partial, _plugins?: KyselyPlugin[]) { super( kysely, fn ?? { @@ -73,13 +81,9 @@ export abstract class PostgresConnection extends Connection { ]; } - protected override async batch( - queries: [...Queries], - ): Promise<{ - [K in keyof Queries]: Awaited>; - }> { + override async executeQueries(...qbs: O): Promise> { 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; } } diff --git a/packages/postgres/src/PostgresIntrospector.ts b/packages/postgres/src/PostgresIntrospector.ts index 82b75ba..4b1c928 100644 --- a/packages/postgres/src/PostgresIntrospector.ts +++ b/packages/postgres/src/PostgresIntrospector.ts @@ -1,5 +1,5 @@ import { type SchemaMetadata, sql } from "kysely"; -import { BaseIntrospector } from "bknd/data"; +import { BaseIntrospector } from "bknd"; type PostgresSchemaSpec = { name: string; diff --git a/packages/postgres/src/PostgresJsConnection.ts b/packages/postgres/src/PostgresJsConnection.ts index 1ab1fe4..deff210 100644 --- a/packages/postgres/src/PostgresJsConnection.ts +++ b/packages/postgres/src/PostgresJsConnection.ts @@ -1,13 +1,15 @@ import { Kysely } from "kysely"; import { PostgresIntrospector } from "./PostgresIntrospector"; import { PostgresConnection, plugins } from "./PostgresConnection"; -import { customIntrospector } from "bknd/data"; +import { customIntrospector } from "bknd"; import { PostgresJSDialect } from "kysely-postgres-js"; import $postgresJs, { type Sql, type Options, type PostgresType } from "postgres"; export type PostgresJsConfig = Options>; export class PostgresJsConnection extends PostgresConnection { + override name = "postgres-js"; + private postgres: Sql; constructor(opts: { postgres: Sql }) { diff --git a/packages/postgres/src/custom.ts b/packages/postgres/src/custom.ts index b7369f1..9d626a0 100644 --- a/packages/postgres/src/custom.ts +++ b/packages/postgres/src/custom.ts @@ -1,9 +1,10 @@ -import type { Constructor } from "bknd/core"; -import { customIntrospector, type DbFunctions } from "bknd/data"; +import { customIntrospector, type DbFunctions } from "bknd"; import { Kysely, type Dialect, type KyselyPlugin } from "kysely"; import { plugins, PostgresConnection } from "./PostgresConnection"; import { PostgresIntrospector } from "./PostgresIntrospector"; +export type Constructor = new (...args: any[]) => T; + export type CustomPostgresConnection = { supports?: PostgresConnection["supported"]; fn?: Partial; @@ -15,17 +16,19 @@ export function createCustomPostgresConnection< T extends Constructor, C extends ConstructorParameters[0], >( + name: string, dialect: Constructor, options?: CustomPostgresConnection, -): (config: C) => PostgresConnection { +): (config: C) => PostgresConnection { const supported = { batching: true, ...((options?.supports ?? {}) as any), }; return (config: C) => - new (class extends PostgresConnection { - protected override readonly supported = supported; + new (class extends PostgresConnection { + override name = name; + override readonly supported = supported; constructor(config: C) { super( diff --git a/packages/postgres/test/suite.ts b/packages/postgres/test/suite.ts index 0d4feec..b5c1cf3 100644 --- a/packages/postgres/test/suite.ts +++ b/packages/postgres/test/suite.ts @@ -1,8 +1,11 @@ import { describe, beforeAll, afterAll, expect, it, afterEach } from "bun:test"; import type { PostgresConnection } from "../src"; -import { createApp } from "bknd"; -import * as proto from "bknd/data"; +import { createApp, em, entity, text } from "bknd"; import { disableConsoleLog, enableConsoleLog } from "bknd/utils"; +// @ts-ignore +import { connectionTestSuite } from "$bknd/data/connection/connection-test-suite"; +// @ts-ignore +import { bunTestRunner } from "$bknd/adapter/bun/test"; export type TestSuiteConfig = { createConnection: () => InstanceType; @@ -12,8 +15,9 @@ export type TestSuiteConfig = { export async function defaultCleanDatabase(connection: InstanceType) { const kysely = connection.kysely; - // drop all tables & create new schema + // drop all tables+indexes & create new schema await kysely.schema.dropSchema("public").ifExists().cascade().execute(); + await kysely.schema.dropIndex("public").ifExists().cascade().execute(); await kysely.schema.createSchema("public").execute(); } @@ -32,6 +36,23 @@ export function testSuite(config: TestSuiteConfig) { beforeAll(() => disableConsoleLog(["log", "warn", "error"])); afterAll(() => enableConsoleLog()); + // @todo: postgres seems to add multiple indexes, thus failing the test suite + /* describe("test suite", () => { + connectionTestSuite(bunTestRunner, { + makeConnection: () => { + const connection = config.createConnection(); + return { + connection, + dispose: async () => { + await cleanDatabase(connection, config); + await connection.close(); + }, + }; + }, + rawDialectDetails: [], + }); + }); */ + describe("base", () => { it("should connect to the database", async () => { const connection = config.createConnection(); @@ -73,14 +94,14 @@ export function testSuite(config: TestSuiteConfig) { }); it("should create a basic schema", async () => { - const schema = proto.em( + const schema = em( { - posts: proto.entity("posts", { - title: proto.text().required(), - content: proto.text(), + posts: entity("posts", { + title: text().required(), + content: text(), }), - comments: proto.entity("comments", { - content: proto.text(), + comments: entity("comments", { + content: text(), }), }, (fns, s) => { @@ -153,20 +174,20 @@ export function testSuite(config: TestSuiteConfig) { }); it("should support uuid", async () => { - const schema = proto.em( + const schema = em( { - posts: proto.entity( + posts: entity( "posts", { - title: proto.text().required(), - content: proto.text(), + title: text().required(), + content: text(), }, { primary_format: "uuid", }, ), - comments: proto.entity("comments", { - content: proto.text(), + comments: entity("comments", { + content: text(), }), }, (fns, s) => { @@ -187,8 +208,8 @@ export function testSuite(config: TestSuiteConfig) { // @ts-expect-error expect(config.data.entities?.posts.fields?.id.config?.format).toBe("uuid"); - const em = app.em; - const mutator = em.mutator(em.entity("posts")); + const $em = app.em; + const mutator = $em.mutator($em.entity("posts")); const data = await mutator.insertOne({ title: "Hello", content: "World" }); expect(data.data.id).toBeString(); expect(String(data.data.id).length).toBe(36); diff --git a/packages/postgres/tsconfig.json b/packages/postgres/tsconfig.json index d2359e0..5bb10f6 100644 --- a/packages/postgres/tsconfig.json +++ b/packages/postgres/tsconfig.json @@ -1,29 +1,33 @@ { - "compilerOptions": { - "composite": false, - "module": "ESNext", - "moduleResolution": "bundler", - "allowImportingTsExtensions": false, - "target": "ES2022", - "noImplicitAny": false, - "allowJs": true, - "verbatimModuleSyntax": true, - "declaration": true, - "strict": true, - "allowUnusedLabels": false, - "allowUnreachableCode": false, - "exactOptionalPropertyTypes": false, - "noFallthroughCasesInSwitch": true, - "noImplicitOverride": true, - "noImplicitReturns": true, - "noPropertyAccessFromIndexSignature": false, - "noUncheckedIndexedAccess": true, - "noUnusedLocals": false, - "noUnusedParameters": false, - "isolatedModules": true, - "esModuleInterop": true, - "skipLibCheck": true - }, - "include": ["./src/**/*.ts"], - "exclude": ["node_modules"] + "compilerOptions": { + "composite": false, + "module": "ESNext", + "moduleResolution": "bundler", + "allowImportingTsExtensions": false, + "target": "ES2022", + "noImplicitAny": false, + "allowJs": true, + "verbatimModuleSyntax": true, + "declaration": true, + "strict": true, + "allowUnusedLabels": false, + "allowUnreachableCode": false, + "exactOptionalPropertyTypes": false, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noPropertyAccessFromIndexSignature": false, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "isolatedModules": true, + "esModuleInterop": true, + "skipLibCheck": true, + "baseUrl": ".", + "paths": { + "$bknd/*": ["../../app/src/*"] + } + }, + "include": ["./src/**/*.ts"], + "exclude": ["node_modules"] } From fea2812688381d644780aa1200317ab8c7ea60ab Mon Sep 17 00:00:00 2001 From: dswbx Date: Sun, 14 Sep 2025 17:03:23 +0200 Subject: [PATCH 4/9] fix: handle numbered object conversion and update MCP tool URL Add `convertNumberedObjectToArray` utility for handling numbered object to array conversion, addressing MCP tool allOf behavior. Adjust MCP tool URL in configuration and ensure default inspect options in development environment. Minor improvement in role enumeration handling for auth. --- .cursor/mcp.json | 5 +---- app/src/auth/api/AuthController.ts | 3 ++- app/src/core/utils/objects.ts | 7 +++++++ app/src/data/api/DataController.ts | 19 +++++++++++++++++-- app/vite.dev.ts | 3 +++ 5 files changed, 30 insertions(+), 7 deletions(-) diff --git a/.cursor/mcp.json b/.cursor/mcp.json index 064ec5c..8cd1d99 100644 --- a/.cursor/mcp.json +++ b/.cursor/mcp.json @@ -1,10 +1,7 @@ { "mcpServers": { "bknd": { - "url": "http://localhost:3000/mcp", - "headers": { - "API_KEY": "value" - } + "url": "http://localhost:28623/api/system/mcp" } } } diff --git a/app/src/auth/api/AuthController.ts b/app/src/auth/api/AuthController.ts index 33c0df6..ba12d4a 100644 --- a/app/src/auth/api/AuthController.ts +++ b/app/src/auth/api/AuthController.ts @@ -221,6 +221,7 @@ export class AuthController extends Controller { return user; }; + const roles = Object.keys(this.auth.config.roles ?? {}); mcp.tool( // @todo: needs permission "auth_user_create", @@ -231,7 +232,7 @@ export class AuthController extends Controller { password: s.string({ minLength: 8 }), role: s .string({ - enum: Object.keys(this.auth.config.roles ?? {}), + enum: roles.length > 0 ? roles : undefined, }) .optional(), }), diff --git a/app/src/core/utils/objects.ts b/app/src/core/utils/objects.ts index 4a5e129..6f6a985 100644 --- a/app/src/core/utils/objects.ts +++ b/app/src/core/utils/objects.ts @@ -473,3 +473,10 @@ export function deepFreeze(object: T): T { return Object.freeze(object); } + +export function convertNumberedObjectToArray(obj: object): any[] | object { + if (Object.keys(obj).every((key) => Number.isInteger(Number(key)))) { + return Object.values(obj); + } + return obj; +} diff --git a/app/src/data/api/DataController.ts b/app/src/data/api/DataController.ts index a0d7f02..055033c 100644 --- a/app/src/data/api/DataController.ts +++ b/app/src/data/api/DataController.ts @@ -1,6 +1,15 @@ import type { ModuleBuildContext } from "modules"; import { Controller } from "modules/Controller"; -import { jsc, s, describeRoute, schemaToSpec, omitKeys, pickKeys, mcpTool } from "bknd/utils"; +import { + jsc, + s, + describeRoute, + schemaToSpec, + omitKeys, + pickKeys, + mcpTool, + convertNumberedObjectToArray, +} from "bknd/utils"; import * as SystemPermissions from "modules/permissions"; import type { AppDataConfig } from "../data-schema"; import type { EntityManager, EntityData } from "data/entities"; @@ -420,7 +429,13 @@ export class DataController extends Controller { if (!this.entityExists(entity)) { return this.notFound(c); } - const body = (await c.req.json()) as EntityData | EntityData[]; + + const _body = (await c.req.json()) as EntityData | EntityData[]; + // @todo: check on jsonv-ts how to handle this better + // temporary fix for numbered object to array + // this happens when the MCP tool uses the allOf function + // to transform all validation targets into a single object + const body = convertNumberedObjectToArray(_body); if (Array.isArray(body)) { const result = await this.em.mutator(entity).insertMany(body); diff --git a/app/vite.dev.ts b/app/vite.dev.ts index bee9219..1bc2309 100644 --- a/app/vite.dev.ts +++ b/app/vite.dev.ts @@ -9,6 +9,9 @@ import { nodeSqlite } from "./src/adapter/node/connection/NodeSqliteConnection"; import { libsql } from "./src/data/connection/sqlite/libsql/LibsqlConnection"; import { $console } from "core/utils/console"; import { createClient } from "@libsql/client"; +import util from "node:util"; + +util.inspect.defaultOptions.depth = 5; registries.media.register("local", StorageLocalAdapter); From d2de642c172b7093301ecfcd67fc6b6272369a7c Mon Sep 17 00:00:00 2001 From: dswbx Date: Mon, 15 Sep 2025 09:25:32 +0200 Subject: [PATCH 5/9] fix: update build config and improve e2e adapter handling Added dynamic inclusion of dependencies as external packages and switched build platform to browser. Streamlined e2e adapter test setup, improved type safety, and standardized console output for better clarity. --- app/build.ts | 5 ++++- app/e2e/adapters.ts | 12 ++++++------ app/package.json | 8 ++++---- app/src/Api.ts | 2 +- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/app/build.ts b/app/build.ts index eb15450..66db700 100644 --- a/app/build.ts +++ b/app/build.ts @@ -61,8 +61,11 @@ function delayTypes() { watcher_timeout = setTimeout(buildTypes, 1000); } +const dependencies = Object.keys(pkg.dependencies); + // collection of always-external packages const external = [ + ...dependencies, "bun:test", "node:test", "node:assert/strict", @@ -87,7 +90,7 @@ async function buildApi() { external: [...external], metafile: true, target: "esnext", - platform: "neutral", + platform: "browser", format: ["esm"], splitting: false, loader: { diff --git a/app/e2e/adapters.ts b/app/e2e/adapters.ts index 9ca8154..0dd009b 100644 --- a/app/e2e/adapters.ts +++ b/app/e2e/adapters.ts @@ -17,7 +17,7 @@ async function run( }); // Read from stdout - const reader = proc.stdout.getReader(); + const reader = (proc.stdout as ReadableStream).getReader(); const decoder = new TextDecoder(); // Function to read chunks @@ -30,7 +30,7 @@ async function run( const text = decoder.decode(value); if (!resolveCalled) { - console.log(c.dim(text.replace(/\n$/, ""))); + console.info(c.dim(text.replace(/\n$/, ""))); } onChunk( text, @@ -189,21 +189,21 @@ const adapters = { async function testAdapter(name: keyof typeof adapters) { const config = adapters[name]; - console.log("adapter", c.cyan(name)); + console.info("adapter", c.cyan(name)); await config.clean(); const { proc, data } = await config.start(); - console.log("proc:", proc.pid, "data:", c.cyan(data)); + console.info("proc:", proc.pid, "data:", c.cyan(data)); //proc.kill();process.exit(0); const add_env = "env" in config && config.env ? config.env : ""; await $`TEST_URL=${data} TEST_ADAPTER=${name} ${add_env} bun run test:e2e`; - console.log("DONE!"); + console.info("DONE!"); while (!proc.killed) { proc.kill("SIGINT"); await Bun.sleep(250); - console.log("Waiting for process to exit..."); + console.info("Waiting for process to exit..."); } } diff --git a/app/package.json b/app/package.json index aea0395..3277694 100644 --- a/app/package.json +++ b/app/package.json @@ -30,7 +30,7 @@ "build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly && tsc-alias", "updater": "bun x npm-check-updates -ui", "cli": "LOCAL=1 bun src/cli/index.ts", - "prepublishOnly": "bun run types && bun run test && bun run test:node && bun run test:e2e && bun run build:all && cp ../README.md ./", + "prepublishOnly": "bun run types && bun run test && bun run test:node && VITE_DB_URL=:memory: bun run test:e2e && bun run build:all && cp ../README.md ./", "postpublish": "rm -f README.md", "test": "ALL_TESTS=1 bun test --bail", "test:all": "bun run test && bun run test:node", @@ -39,8 +39,8 @@ "test:adapters": "bun test src/adapter/**/*.adapter.spec.ts --bail", "test:coverage": "ALL_TESTS=1 bun test --bail --coverage", "test:vitest:coverage": "vitest run --coverage", - "test:e2e": "VITE_DB_URL=:memory: playwright test", - "test:e2e:adapters": "VITE_DB_URL=:memory: bun run e2e/adapters.ts", + "test:e2e": "playwright test", + "test:e2e:adapters": "bun run e2e/adapters.ts", "test:e2e:ui": "VITE_DB_URL=:memory: playwright test --ui", "test:e2e:debug": "VITE_DB_URL=:memory: playwright test --debug", "test:e2e:report": "VITE_DB_URL=:memory: playwright show-report", @@ -71,6 +71,7 @@ "oauth4webapi": "^2.11.1", "object-path-immutable": "^4.1.2", "radix-ui": "^1.1.3", + "picocolors": "^1.1.1", "swr": "^2.3.3" }, "devDependencies": { @@ -108,7 +109,6 @@ "libsql-stateless-easy": "^1.8.0", "open": "^10.1.0", "openapi-types": "^12.1.3", - "picocolors": "^1.1.1", "postcss": "^8.5.3", "postcss-preset-mantine": "^1.17.0", "postcss-simple-vars": "^7.0.1", diff --git a/app/src/Api.ts b/app/src/Api.ts index d9fb8e4..28d2ef0 100644 --- a/app/src/Api.ts +++ b/app/src/Api.ts @@ -43,7 +43,7 @@ export type ApiOptions = { } & ( | { token?: string; - user?: TApiUser; + user?: TApiUser | null; } | { request: Request; From f23135322897e8584f9ef32ac9e0377656cce296 Mon Sep 17 00:00:00 2001 From: dswbx Date: Mon, 15 Sep 2025 09:31:01 +0200 Subject: [PATCH 6/9] bump version to 0.17.2-rc.1 --- app/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/package.json b/app/package.json index 3277694..6fdfa72 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "type": "module", "sideEffects": false, "bin": "./dist/cli/index.js", - "version": "0.17.1", + "version": "0.17.2-rc.1", "description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.", "homepage": "https://bknd.io", "repository": { From 9a1589da0be0faca667c87f4c039abf38a2d3582 Mon Sep 17 00:00:00 2001 From: dswbx Date: Mon, 15 Sep 2025 10:07:34 +0200 Subject: [PATCH 7/9] bump @bknd/postgres version to 0.2.0 --- packages/postgres/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/postgres/package.json b/packages/postgres/package.json index e822af4..036d6a9 100644 --- a/packages/postgres/package.json +++ b/packages/postgres/package.json @@ -1,6 +1,6 @@ { "name": "@bknd/postgres", - "version": "0.1.0", + "version": "0.2.0", "type": "module", "main": "dist/index.js", "module": "dist/index.js", From 1ef5bf7d15242275a5fbaec870c16e362a1c6de0 Mon Sep 17 00:00:00 2001 From: dswbx Date: Mon, 15 Sep 2025 10:09:51 +0200 Subject: [PATCH 8/9] fix: adjust mcp schema validation and add default path support Updated `mcp.json` to enhance validation by supporting `anyOf` schema for `entity` properties and refined `role` typing. Introduced default `path` property for MCP system API and aligned build assets configuration accordingly. --- app/internal/docs.build-assets.ts | 1 + docs/mcp.json | 157 ++++++++++++++++++++++-------- 2 files changed, 116 insertions(+), 42 deletions(-) diff --git a/app/internal/docs.build-assets.ts b/app/internal/docs.build-assets.ts index 127def7..730367f 100644 --- a/app/internal/docs.build-assets.ts +++ b/app/internal/docs.build-assets.ts @@ -7,6 +7,7 @@ async function generate() { server: { mcp: { enabled: true, + path: "/mcp", }, }, auth: { diff --git a/docs/mcp.json b/docs/mcp.json index df16c4b..7075b1e 100644 --- a/docs/mcp.json +++ b/docs/mcp.json @@ -35,8 +35,7 @@ "minLength": 8 }, "role": { - "type": "string", - "enum": [] + "type": "string" } }, "required": [ @@ -134,10 +133,17 @@ ], "properties": { "entity": { - "type": "string", - "enum": [ - "users", - "media" + "anyOf": [ + { + "type": "string", + "enum": [ + "users", + "media" + ] + }, + { + "type": "string" + } ], "$target": "param" }, @@ -161,10 +167,17 @@ ], "properties": { "entity": { - "type": "string", - "enum": [ - "users", - "media" + "anyOf": [ + { + "type": "string", + "enum": [ + "users", + "media" + ] + }, + { + "type": "string" + } ], "$target": "param" }, @@ -194,10 +207,17 @@ ], "properties": { "entity": { - "type": "string", - "enum": [ - "users", - "media" + "anyOf": [ + { + "type": "string", + "enum": [ + "users", + "media" + ] + }, + { + "type": "string" + } ], "$target": "param" }, @@ -235,10 +255,17 @@ ], "properties": { "entity": { - "type": "string", - "enum": [ - "users", - "media" + "anyOf": [ + { + "type": "string", + "enum": [ + "users", + "media" + ] + }, + { + "type": "string" + } ], "$target": "param" }, @@ -276,10 +303,17 @@ ], "properties": { "entity": { - "type": "string", - "enum": [ - "users", - "media" + "anyOf": [ + { + "type": "string", + "enum": [ + "users", + "media" + ] + }, + { + "type": "string" + } ], "$target": "param" } @@ -297,10 +331,17 @@ ], "properties": { "entity": { - "type": "string", - "enum": [ - "users", - "media" + "anyOf": [ + { + "type": "string", + "enum": [ + "users", + "media" + ] + }, + { + "type": "string" + } ], "$target": "param" }, @@ -334,10 +375,17 @@ ], "properties": { "entity": { - "type": "string", - "enum": [ - "users", - "media" + "anyOf": [ + { + "type": "string", + "enum": [ + "users", + "media" + ] + }, + { + "type": "string" + } ], "$target": "param" }, @@ -410,10 +458,17 @@ ], "properties": { "entity": { - "type": "string", - "enum": [ - "users", - "media" + "anyOf": [ + { + "type": "string", + "enum": [ + "users", + "media" + ] + }, + { + "type": "string" + } ], "$target": "param" }, @@ -462,10 +517,17 @@ ], "properties": { "entity": { - "type": "string", - "enum": [ - "users", - "media" + "anyOf": [ + { + "type": "string", + "enum": [ + "users", + "media" + ] + }, + { + "type": "string" + } ], "$target": "param" }, @@ -494,10 +556,17 @@ ], "properties": { "entity": { - "type": "string", - "enum": [ - "users", - "media" + "anyOf": [ + { + "type": "string", + "enum": [ + "users", + "media" + ] + }, + { + "type": "string" + } ], "$target": "param" }, @@ -3994,6 +4063,10 @@ "enabled": { "type": "boolean", "default": false + }, + "path": { + "type": "string", + "default": "/api/system/mcp" } } } From 655bb241a80d20f7c60db38878f08a01cb968c8f Mon Sep 17 00:00:00 2001 From: dswbx Date: Mon, 15 Sep 2025 10:14:14 +0200 Subject: [PATCH 9/9] bump version to 0.17.2 --- app/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/package.json b/app/package.json index 6fdfa72..98d9426 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "type": "module", "sideEffects": false, "bin": "./dist/cli/index.js", - "version": "0.17.2-rc.1", + "version": "0.17.2", "description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.", "homepage": "https://bknd.io", "repository": {