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

@@ -61,8 +61,11 @@ function delayTypes() {
watcher_timeout = setTimeout(buildTypes, 1000);
}
const dependencies = Object.keys(pkg.dependencies);
// collection of always-external packages
const external = [
...dependencies,
"bun:test",
"node:test",
"node:assert/strict",
@@ -86,10 +89,10 @@ async function buildApi() {
outDir: "dist",
external: [...external],
metafile: true,
target: "esnext",
platform: "browser",
format: ["esm"],
splitting: false,
treeshake: true,
loader: {
".svg": "dataurl",
},
@@ -245,6 +248,8 @@ async function buildAdapters() {
// base adapter handles
tsup.build({
...baseConfig(""),
target: "esnext",
platform: "neutral",
entry: ["src/adapter/index.ts"],
outDir: "dist/adapter",
}),

View File

@@ -17,7 +17,7 @@ async function run(
});
// Read from stdout
const reader = proc.stdout.getReader();
const reader = (proc.stdout as ReadableStream).getReader();
const decoder = new TextDecoder();
// Function to read chunks
@@ -30,7 +30,7 @@ async function run(
const text = decoder.decode(value);
if (!resolveCalled) {
console.log(c.dim(text.replace(/\n$/, "")));
console.info(c.dim(text.replace(/\n$/, "")));
}
onChunk(
text,
@@ -189,21 +189,21 @@ const adapters = {
async function testAdapter(name: keyof typeof adapters) {
const config = adapters[name];
console.log("adapter", c.cyan(name));
console.info("adapter", c.cyan(name));
await config.clean();
const { proc, data } = await config.start();
console.log("proc:", proc.pid, "data:", c.cyan(data));
console.info("proc:", proc.pid, "data:", c.cyan(data));
//proc.kill();process.exit(0);
const add_env = "env" in config && config.env ? config.env : "";
await $`TEST_URL=${data} TEST_ADAPTER=${name} ${add_env} bun run test:e2e`;
console.log("DONE!");
console.info("DONE!");
while (!proc.killed) {
proc.kill("SIGINT");
await Bun.sleep(250);
console.log("Waiting for process to exit...");
console.info("Waiting for process to exit...");
}
}

View File

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

View File

@@ -15,7 +15,7 @@
},
"packageManager": "bun@1.2.19",
"engines": {
"node": ">=22"
"node": ">=22.13"
},
"scripts": {
"dev": "BKND_CLI_LOG_LEVEL=debug vite",
@@ -30,7 +30,7 @@
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly && tsc-alias",
"updater": "bun x npm-check-updates -ui",
"cli": "LOCAL=1 bun src/cli/index.ts",
"prepublishOnly": "bun run types && bun run test && bun run test:node && bun run test:e2e && bun run build:all && cp ../README.md ./",
"prepublishOnly": "bun run types && bun run test && bun run test:node && VITE_DB_URL=:memory: bun run test:e2e && bun run build:all && cp ../README.md ./",
"postpublish": "rm -f README.md",
"test": "ALL_TESTS=1 bun test --bail",
"test:all": "bun run test && bun run test:node",
@@ -39,8 +39,8 @@
"test:adapters": "bun test src/adapter/**/*.adapter.spec.ts --bail",
"test:coverage": "ALL_TESTS=1 bun test --bail --coverage",
"test:vitest:coverage": "vitest run --coverage",
"test:e2e": "VITE_DB_URL=:memory: playwright test",
"test:e2e:adapters": "VITE_DB_URL=:memory: bun run e2e/adapters.ts",
"test:e2e": "playwright test",
"test:e2e:adapters": "bun run e2e/adapters.ts",
"test:e2e:ui": "VITE_DB_URL=:memory: playwright test --ui",
"test:e2e:debug": "VITE_DB_URL=:memory: playwright test --debug",
"test:e2e:report": "VITE_DB_URL=:memory: playwright show-report",
@@ -71,6 +71,7 @@
"oauth4webapi": "^2.11.1",
"object-path-immutable": "^4.1.2",
"radix-ui": "^1.1.3",
"picocolors": "^1.1.1",
"swr": "^2.3.3"
},
"devDependencies": {
@@ -108,7 +109,6 @@
"libsql-stateless-easy": "^1.8.0",
"open": "^10.1.0",
"openapi-types": "^12.1.3",
"picocolors": "^1.1.1",
"postcss": "^8.5.3",
"postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1",

View File

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

View File

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

View File

@@ -221,6 +221,7 @@ export class AuthController extends Controller {
return user;
};
const roles = Object.keys(this.auth.config.roles ?? {});
mcp.tool(
// @todo: needs permission
"auth_user_create",
@@ -231,7 +232,7 @@ export class AuthController extends Controller {
password: s.string({ minLength: 8 }),
role: s
.string({
enum: Object.keys(this.auth.config.roles ?? {}),
enum: roles.length > 0 ? roles : undefined,
})
.optional(),
}),

View File

@@ -505,3 +505,10 @@ export function deepFreeze<T extends object>(object: T): T {
return Object.freeze(object);
}
export function convertNumberedObjectToArray(obj: object): any[] | object {
if (Object.keys(obj).every((key) => Number.isInteger(Number(key)))) {
return Object.values(obj);
}
return obj;
}

View File

@@ -1,10 +1,16 @@
import { v4, v7 } from "uuid";
import { v4, v7, validate, version as uuidVersion } from "uuid";
// generates v4
export function uuid(): string {
return v4();
return v4();
}
// generates v7
export function uuidv7(): string {
return v7();
return v7();
}
// validate uuid
export function uuidValidate(uuid: string, version: 4 | 7): boolean {
return validate(uuid) && uuidVersion(uuid) === version;
}

View File

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

View File

@@ -258,6 +258,9 @@ export class EntityManager<TBD extends object = DefaultDB> {
// @todo: centralize and add tests
hydrate(entity_name: string, _data: EntityData[]) {
if (!Array.isArray(_data) || _data.length === 0) {
return [];
}
const entity = this.entity(entity_name);
const data: EntityData[] = [];

View File

@@ -11,6 +11,7 @@ import { css, Style } from "hono/css";
import { Controller } from "modules/Controller";
import * as SystemPermissions from "modules/permissions";
import type { TApiUser } from "Api";
import type { Manifest } from "vite";
const htmlBkndContextReplace = "<!-- BKND_CONTEXT -->";
@@ -32,6 +33,7 @@ export type AdminControllerOptions = {
debugRerenders?: boolean;
theme?: "dark" | "light" | "system";
logoReturnPath?: string;
manifest?: Manifest;
};
export class AdminController extends Controller {
@@ -194,8 +196,10 @@ export class AdminController extends Controller {
};
if (isProd) {
let manifest: any;
if (this.options.assetsPath.startsWith("http")) {
let manifest: Manifest;
if (this.options.manifest) {
manifest = this.options.manifest;
} else if (this.options.assetsPath.startsWith("http")) {
manifest = await fetch(this.options.assetsPath + ".vite/manifest.json", {
headers: {
Accept: "application/json",
@@ -204,14 +208,14 @@ export class AdminController extends Controller {
} else {
// @ts-ignore
manifest = await import("bknd/dist/manifest.json", {
assert: { type: "json" },
with: { type: "json" },
}).then((res) => res.default);
}
try {
// @todo: load all marked as entry (incl. css)
assets.js = manifest["src/ui/main.tsx"].file;
assets.css = manifest["src/ui/main.tsx"].css[0] as any;
assets.js = manifest["src/ui/main.tsx"]?.file!;
assets.css = manifest["src/ui/main.tsx"]?.css?.[0] as any;
} catch (e) {
$console.warn("Couldn't find assets in manifest", e);
}

View File

@@ -9,6 +9,9 @@ import { nodeSqlite } from "./src/adapter/node/connection/NodeSqliteConnection";
import { libsql } from "./src/data/connection/sqlite/libsql/LibsqlConnection";
import { $console } from "core/utils/console";
import { createClient } from "@libsql/client";
import util from "node:util";
util.inspect.defaultOptions.depth = 5;
registries.media.register("local", StorageLocalAdapter);