Compare commits

..

17 Commits

Author SHA1 Message Date
dswbx 8c26d646bd fix env test, prefer source over process 2025-11-11 14:02:43 +01:00
dswbx 4859b99954 feat: remove cloudflare adapter dependency to nodejs_compat
- adjusted bindings extraction to not rely on node inspect anymore
- added new vite export that uses nodejs apis
2025-11-11 13:33:04 +01:00
dswbx 3f55c0f2c5 fix imports and adjust biome and linting configuration
remove unused `SecretSchema` import, adjust biome settings to modify linting behavior, and clean formatting in `slugify` and other functions.
2025-11-11 13:29:24 +01:00
dswbx 9ec31373e1 Merge pull request #295 from bknd-io/fix/hybrid-schema-sync
fix hybrid schema sync
2025-11-10 10:36:52 +01:00
dswbx 70b25a9f9b hybrid: fix timing for automatic schema syncs to work with plugins 2025-11-10 10:34:50 +01:00
dswbx 300d86ff7c Merge pull request #293 from bknd-io/chore/biome-upgrade
upgrade biome and vscode settings
2025-11-07 09:49:22 +01:00
dswbx 4c9b662f6f fix bknd/util imports 2025-11-07 09:47:43 +01:00
dswbx a862cfdcf1 add .vscode/settings.json 2025-11-07 09:45:12 +01:00
dswbx de62f4e729 upgrade biome config 2025-11-07 09:39:02 +01:00
dswbx 4575d89cfe Merge pull request #290 from bknd-io/chore/upgrade-deps
upgrade deps
2025-11-05 10:03:36 +01:00
dswbx 0ca0828581 fix examples 2025-11-05 09:37:49 +01:00
dswbx 07a57ebf67 fix cli build script, add commander, fix types 2025-11-05 08:27:47 +01:00
dswbx e9f1241ec3 fix tests: remove .only 2025-11-05 08:20:36 +01:00
dswbx 80903d4ffa upgrade bun, remove hoisting 2025-11-05 08:15:41 +01:00
dswbx c8290802e9 upgrade biome, typescript, icons, other dev deps. also keep hoisting for now 2025-11-05 07:48:33 +01:00
dswbx ac6cd4a900 upgrade vite + fix types 2025-10-31 21:40:55 +01:00
dswbx a91ddb1ec3 update deps 2025-10-31 21:33:11 +01:00
48 changed files with 1331 additions and 1263 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
- name: Setup Bun - name: Setup Bun
uses: oven-sh/setup-bun@v1 uses: oven-sh/setup-bun@v1
with: with:
bun-version: "1.2.22" bun-version: "1.3.1"
- name: Install dependencies - name: Install dependencies
working-directory: ./app working-directory: ./app
+2 -1
View File
@@ -27,7 +27,8 @@ packages/media/.env
.npmrc .npmrc
/.verdaccio /.verdaccio
.idea .idea
.vscode .vscode/*
!.vscode/settings.json
.git_old .git_old
docker/tmp docker/tmp
.debug .debug
+19
View File
@@ -0,0 +1,19 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"biome.enabled": true,
"editor.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit",
"source.fixAll.biome": "explicit"
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.preferences.autoImportFileExcludePatterns": [
"**/dist/**",
"**/node_modules/**/dist/**",
"**/node_modules/**/!(src|lib|esm)/**" // optional, stricter
],
"typescript.preferences.includePackageJsonAutoImports": "on",
"typescript.tsserver.watchOptions": {
"excludeDirectories": ["**/dist", "**/node_modules/**/dist"]
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"typescript.preferences.includePackageJsonAutoImports": "off",
"typescript.suggest.autoImports": true,
"typescript.preferences.importModuleSpecifier": "relative",
"search.exclude": {
"**/dist/**": true,
"**/node_modules/**": true
},
"files.exclude": {
"**/dist/**": true
}
}
+1 -1
View File
@@ -108,7 +108,7 @@ describe("App tests", async () => {
expect(Array.from(app.plugins.keys())).toEqual(["test"]); expect(Array.from(app.plugins.keys())).toEqual(["test"]);
}); });
test.only("drivers", async () => { test("drivers", async () => {
const called: string[] = []; const called: string[] = [];
const app = new App(dummyConnection, undefined, { const app = new App(dummyConnection, undefined, {
drivers: { drivers: {
+1 -1
View File
@@ -15,7 +15,7 @@ const mockedBackend = new Hono()
.get("/file/:name", async (c) => { .get("/file/:name", async (c) => {
const { name } = c.req.param(); const { name } = c.req.param();
const file = Bun.file(`${assetsPath}/${name}`); const file = Bun.file(`${assetsPath}/${name}`);
return new Response(file, { return new Response(new File([await file.bytes()], name, { type: file.type }), {
headers: { headers: {
"Content-Type": file.type, "Content-Type": file.type,
"Content-Length": file.size.toString(), "Content-Length": file.size.toString(),
+1 -1
View File
@@ -76,7 +76,7 @@ describe("repros", async () => {
expect(app.em.entities.map((e) => e.name)).toEqual(["media", "test"]); expect(app.em.entities.map((e) => e.name)).toEqual(["media", "test"]);
}); });
test.only("verify inversedBy", async () => { test("verify inversedBy", async () => {
const schema = proto.em( const schema = proto.em(
{ {
products: proto.entity("products", { products: proto.entity("products", {
@@ -7,9 +7,7 @@ import v7 from "./samples/v7.json";
import v8 from "./samples/v8.json"; import v8 from "./samples/v8.json";
import v8_2 from "./samples/v8-2.json"; import v8_2 from "./samples/v8-2.json";
import v9 from "./samples/v9.json"; import v9 from "./samples/v9.json";
import v10 from "./samples/v10.json";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test"; import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
import { CURRENT_VERSION } from "modules/db/migrations";
beforeAll(() => disableConsoleLog()); beforeAll(() => disableConsoleLog());
afterAll(enableConsoleLog); afterAll(enableConsoleLog);
@@ -63,7 +61,7 @@ async function getRawConfig(
return await db return await db
.selectFrom("__bknd") .selectFrom("__bknd")
.selectAll() .selectAll()
.where("version", "=", opts?.version ?? CURRENT_VERSION) .$if(!!opts?.version, (qb) => qb.where("version", "=", opts?.version))
.$if((opts?.types?.length ?? 0) > 0, (qb) => qb.where("type", "in", opts?.types)) .$if((opts?.types?.length ?? 0) > 0, (qb) => qb.where("type", "in", opts?.types))
.execute(); .execute();
} }
@@ -117,6 +115,7 @@ describe("Migrations", () => {
"^^s3.secret_access_key^^", "^^s3.secret_access_key^^",
); );
const [config, secrets] = (await getRawConfig(app, { const [config, secrets] = (await getRawConfig(app, {
version: 10,
types: ["config", "secrets"], types: ["config", "secrets"],
})) as any; })) as any;
@@ -130,15 +129,4 @@ describe("Migrations", () => {
"^^s3.secret_access_key^^", "^^s3.secret_access_key^^",
); );
}); });
test("migration from 10 to 11", async () => {
expect(v10.version).toBe(10);
expect(v10.data.entities.test.fields.title.config.fillable).toEqual(["read", "update"]);
const app = await createVersionedApp(v10);
expect(app.version()).toBeGreaterThan(10);
const [config] = (await getRawConfig(app, { types: ["config"] })) as any;
expect(config.json.data.entities.test.fields.title.config.fillable).toEqual(true);
});
}); });
@@ -1,270 +0,0 @@
{
"version": 10,
"server": {
"cors": {
"origin": "*",
"allow_methods": ["GET", "POST", "PATCH", "PUT", "DELETE"],
"allow_headers": [
"Content-Type",
"Content-Length",
"Authorization",
"Accept"
],
"allow_credentials": true
},
"mcp": {
"enabled": true,
"path": "/api/system/mcp",
"logLevel": "warning"
}
},
"data": {
"basepath": "/api/data",
"default_primary_format": "integer",
"entities": {
"test": {
"type": "regular",
"fields": {
"id": {
"type": "primary",
"config": {
"format": "integer",
"fillable": false,
"required": false
}
},
"title": {
"type": "text",
"config": {
"required": false,
"fillable": ["read", "update"]
}
},
"status": {
"type": "enum",
"config": {
"default_value": "INACTIVE",
"options": {
"type": "strings",
"values": ["INACTIVE", "SUBSCRIBED", "UNSUBSCRIBED"]
},
"required": true,
"fillable": true
}
},
"created_at": {
"type": "date",
"config": {
"type": "datetime",
"required": true,
"fillable": true
}
},
"schema": {
"type": "jsonschema",
"config": {
"default_from_schema": true,
"schema": {
"type": "object",
"properties": {
"one": {
"type": "number",
"default": 1
}
}
},
"required": true,
"fillable": true
}
},
"text": {
"type": "text",
"config": {
"required": false,
"fillable": true
}
}
},
"config": {
"sort_field": "id",
"sort_dir": "asc"
}
},
"items": {
"type": "regular",
"fields": {
"id": {
"type": "primary",
"config": {
"format": "integer",
"fillable": false,
"required": false
}
},
"title": {
"type": "text",
"config": {
"required": false,
"fillable": true
}
}
},
"config": {
"sort_field": "id",
"sort_dir": "asc"
}
},
"media": {
"type": "system",
"fields": {
"id": {
"type": "primary",
"config": {
"format": "integer",
"fillable": false,
"required": false
}
},
"path": {
"type": "text",
"config": {
"required": true,
"fillable": true
}
},
"folder": {
"type": "boolean",
"config": {
"default_value": false,
"hidden": true,
"fillable": ["create"],
"required": false
}
},
"mime_type": {
"type": "text",
"config": {
"required": false,
"fillable": true
}
},
"size": {
"type": "number",
"config": {
"required": false,
"fillable": true
}
},
"scope": {
"type": "text",
"config": {
"hidden": true,
"fillable": ["create"],
"required": false
}
},
"etag": {
"type": "text",
"config": {
"required": false,
"fillable": true
}
},
"modified_at": {
"type": "date",
"config": {
"type": "datetime",
"required": false,
"fillable": true
}
},
"reference": {
"type": "text",
"config": {
"required": false,
"fillable": true
}
},
"entity_id": {
"type": "text",
"config": {
"required": false,
"fillable": true
}
},
"metadata": {
"type": "json",
"config": {
"required": false,
"fillable": true
}
}
},
"config": {
"sort_field": "id",
"sort_dir": "asc"
}
}
},
"relations": {},
"indices": {
"idx_unique_media_path": {
"entity": "media",
"fields": ["path"],
"unique": true
},
"idx_media_reference": {
"entity": "media",
"fields": ["reference"],
"unique": false
},
"idx_media_entity_id": {
"entity": "media",
"fields": ["entity_id"],
"unique": false
}
}
},
"auth": {
"enabled": false,
"basepath": "/api/auth",
"entity_name": "users",
"allow_register": true,
"jwt": {
"secret": "",
"alg": "HS256",
"fields": ["id", "email", "role"]
},
"cookie": {
"path": "/",
"sameSite": "lax",
"secure": true,
"httpOnly": true,
"expires": 604800,
"partitioned": false,
"renew": true,
"pathSuccess": "/",
"pathLoggedOut": "/"
},
"strategies": {
"password": {
"type": "password",
"enabled": true,
"config": {
"hashing": "sha256"
}
}
},
"guard": {
"enabled": false
},
"roles": {}
},
"media": {
"enabled": false
},
"flows": {
"basepath": "/api/flows",
"flows": {}
}
}
-20
View File
@@ -1,30 +1,10 @@
import pkg from "./package.json" with { type: "json" }; import pkg from "./package.json" with { type: "json" };
import c from "picocolors"; import c from "picocolors";
import { formatNumber } from "bknd/utils"; import { formatNumber } from "bknd/utils";
import * as esbuild from "esbuild";
const deps = Object.keys(pkg.dependencies); const deps = Object.keys(pkg.dependencies);
const external = ["jsonv-ts/*", "wrangler", "bknd", "bknd/*", ...deps]; const external = ["jsonv-ts/*", "wrangler", "bknd", "bknd/*", ...deps];
if (process.env.DEBUG) {
const result = await esbuild.build({
entryPoints: ["./src/cli/index.ts"],
outdir: "./dist/cli",
platform: "node",
minify: true,
format: "esm",
metafile: true,
bundle: true,
external,
define: {
__isDev: "0",
__version: JSON.stringify(pkg.version),
},
});
await Bun.write("./dist/cli/metafile-esm.json", JSON.stringify(result.metafile, null, 2));
process.exit(0);
}
const result = await Bun.build({ const result = await Bun.build({
entrypoints: ["./src/cli/index.ts"], entrypoints: ["./src/cli/index.ts"],
target: "node", target: "node",
+13 -2
View File
@@ -1,3 +1,4 @@
/** biome-ignore-all lint/suspicious/noConsole: . */
import { $ } from "bun"; import { $ } from "bun";
import * as tsup from "tsup"; import * as tsup from "tsup";
import pkg from "./package.json" with { type: "json" }; import pkg from "./package.json" with { type: "json" };
@@ -96,6 +97,7 @@ async function buildApi() {
metafile: true, metafile: true,
target: "esnext", target: "esnext",
platform: "browser", platform: "browser",
removeNodeProtocol: false,
format: ["esm"], format: ["esm"],
splitting: false, splitting: false,
loader: { loader: {
@@ -228,6 +230,7 @@ function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsu
outDir: `dist/adapter/${adapter}`, outDir: `dist/adapter/${adapter}`,
metafile: true, metafile: true,
splitting: false, splitting: false,
removeNodeProtocol: false,
onSuccess: async () => { onSuccess: async () => {
delayTypes(); delayTypes();
oldConsole.log(c.cyan("[Adapter]"), adapter || "base", c.green("built")); oldConsole.log(c.cyan("[Adapter]"), adapter || "base", c.green("built"));
@@ -265,7 +268,7 @@ async function buildAdapters() {
tsup.build(baseConfig("react-router")), tsup.build(baseConfig("react-router")),
tsup.build( tsup.build(
baseConfig("bun", { baseConfig("bun", {
external: [/^bun\:.*/], external: [/^bun:.*/],
}), }),
), ),
tsup.build(baseConfig("astro")), tsup.build(baseConfig("astro")),
@@ -284,6 +287,14 @@ async function buildAdapters() {
external: [/bknd/, "wrangler", "node:process"], external: [/bknd/, "wrangler", "node:process"],
}), }),
), ),
tsup.build(
baseConfig("cloudflare/vite", {
target: "esnext",
entry: ["src/adapter/cloudflare/vite.ts"],
outDir: "dist/adapter/cloudflare",
metafile: false,
}),
),
tsup.build({ tsup.build({
...baseConfig("vite"), ...baseConfig("vite"),
@@ -320,7 +331,7 @@ async function buildAdapters() {
entry: ["src/adapter/sqlite/bun.ts"], entry: ["src/adapter/sqlite/bun.ts"],
outDir: "dist/adapter/sqlite", outDir: "dist/adapter/sqlite",
metafile: false, metafile: false,
external: [/^bun\:.*/], external: [/^bun:.*/],
}), }),
]); ]);
} }
-6
View File
@@ -1,6 +0,0 @@
[install]
#registry = "http://localhost:4873"
[test]
coverageSkipTestFiles = true
console.depth = 10
+59 -49
View File
@@ -13,7 +13,7 @@
"bugs": { "bugs": {
"url": "https://github.com/bknd-io/bknd/issues" "url": "https://github.com/bknd-io/bknd/issues"
}, },
"packageManager": "bun@1.2.22", "packageManager": "bun@1.3.1",
"engines": { "engines": {
"node": ">=22.13" "node": ">=22.13"
}, },
@@ -49,93 +49,98 @@
"license": "FSL-1.1-MIT", "license": "FSL-1.1-MIT",
"dependencies": { "dependencies": {
"@cfworker/json-schema": "^4.1.1", "@cfworker/json-schema": "^4.1.1",
"@codemirror/lang-html": "^6.4.9", "@codemirror/lang-html": "^6.4.11",
"@codemirror/lang-json": "^6.0.1", "@codemirror/lang-json": "^6.0.2",
"@hello-pangea/dnd": "^18.0.1", "@hello-pangea/dnd": "^18.0.1",
"@hono/swagger-ui": "^0.5.1", "@hono/swagger-ui": "^0.5.2",
"@mantine/core": "^7.17.1", "@mantine/core": "^7.17.1",
"@mantine/hooks": "^7.17.1", "@mantine/hooks": "^7.17.1",
"@tanstack/react-form": "^1.0.5", "@tanstack/react-form": "^1.0.5",
"@uiw/react-codemirror": "^4.23.10", "@uiw/react-codemirror": "^4.25.2",
"@xyflow/react": "^12.4.4", "@xyflow/react": "^12.9.2",
"aws4fetch": "^1.0.20", "aws4fetch": "^1.0.20",
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.3",
"dayjs": "^1.11.13", "dayjs": "^1.11.19",
"fast-xml-parser": "^5.0.8", "fast-xml-parser": "^5.3.1",
"hono": "4.8.3", "hono": "4.10.4",
"json-schema-library": "10.0.0-rc7", "json-schema-library": "10.0.0-rc7",
"json-schema-to-ts": "^3.1.1", "json-schema-to-ts": "^3.1.1",
"jsonv-ts": "0.9.1", "jsonv-ts": "0.9.3",
"kysely": "0.27.6", "kysely": "0.28.8",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"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",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"swr": "^2.3.3" "radix-ui": "^1.1.3",
"swr": "^2.3.6",
"use-sync-external-store": "^1.6.0",
"zustand": "^4"
}, },
"devDependencies": { "devDependencies": {
"@aws-sdk/client-s3": "^3.758.0", "@aws-sdk/client-s3": "^3.922.0",
"@bluwy/giget-core": "^0.1.2", "@bluwy/giget-core": "^0.1.6",
"@clack/prompts": "^0.11.0", "@clack/prompts": "^0.11.0",
"@cloudflare/vitest-pool-workers": "^0.9.3", "@cloudflare/vitest-pool-workers": "^0.10.4",
"@cloudflare/workers-types": "^4.20250606.0", "@cloudflare/workers-types": "^4.20251014.0",
"@dagrejs/dagre": "^1.1.4", "@dagrejs/dagre": "^1.1.4",
"@hono/vite-dev-server": "^0.21.0", "@hono/vite-dev-server": "^0.23.0",
"@hookform/resolvers": "^4.1.3", "@hookform/resolvers": "^5.2.2",
"@libsql/client": "^0.15.9", "@libsql/client": "^0.15.15",
"@mantine/modals": "^7.17.1", "@mantine/modals": "^7.17.1",
"@mantine/notifications": "^7.17.1", "@mantine/notifications": "^7.17.1",
"@playwright/test": "^1.51.1", "@playwright/test": "^1.56.1",
"@rjsf/core": "5.22.2", "@rjsf/core": "5.22.2",
"@rjsf/utils": "5.22.0",
"@standard-schema/spec": "^1.0.0", "@standard-schema/spec": "^1.0.0",
"@tabler/icons-react": "3.18.0", "@tabler/icons-react": "3.35.0",
"@tailwindcss/postcss": "^4.0.12", "@tailwindcss/postcss": "^4.1.16",
"@tailwindcss/vite": "^4.0.12", "@tailwindcss/vite": "^4.1.16",
"@tanstack/react-store": "^0.8.0",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0", "@testing-library/react": "^16.2.0",
"@types/node": "^22.13.10", "@types/node": "^24.10.0",
"@types/react": "^19.0.10", "@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^5.1.0",
"@vitest/coverage-v8": "^3.0.9", "@vitest/coverage-v8": "3.0.9",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dotenv": "^16.4.7", "commander": "^14.0.2",
"dotenv": "^17.2.3",
"jotai": "^2.12.2", "jotai": "^2.12.2",
"jsdom": "^26.0.0", "jsdom": "^26.1.0",
"kysely-d1": "^0.3.0",
"kysely-generic-sqlite": "^1.2.1", "kysely-generic-sqlite": "^1.2.1",
"libsql": "^0.5.22",
"libsql-stateless-easy": "^1.8.0", "libsql-stateless-easy": "^1.8.0",
"open": "^10.1.0", "miniflare": "^4.20251011.2",
"open": "^10.2.0",
"openapi-types": "^12.1.3", "openapi-types": "^12.1.3",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"postcss-preset-mantine": "^1.17.0", "postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1", "postcss-simple-vars": "^7.0.1",
"posthog-js-lite": "^3.4.2", "posthog-js-lite": "^3.6.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-hook-form": "^7.54.2", "react-hook-form": "^7.66.0",
"react-icons": "5.2.1", "react-icons": "5.5.0",
"react-json-view-lite": "^2.4.1", "react-json-view-lite": "^2.5.0",
"sql-formatter": "^15.4.11", "sql-formatter": "^15.6.10",
"tailwind-merge": "^3.0.2", "tailwind-merge": "^3.0.2",
"tailwindcss": "^4.0.12", "tailwindcss": "^4.1.16",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"tsc-alias": "^1.8.11", "tsc-alias": "^1.8.16",
"tsup": "^8.4.0", "tsup": "^8.5.0",
"tsx": "^4.19.3", "tsx": "^4.20.6",
"uuid": "^11.1.0", "uuid": "^13.0.0",
"vite": "^6.3.5", "vite": "^7.1.12",
"vite-plugin-circular-dependency": "^0.5.0", "vite-plugin-circular-dependency": "^0.5.0",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.9", "vitest": "3.0.9",
"wouter": "^3.6.0", "wouter": "^3.7.1",
"wrangler": "^4.37.1", "wrangler": "^4.45.4"
"miniflare": "^4.20250913.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@hono/node-server": "^1.14.3" "@hono/node-server": "^1.19.6"
}, },
"peerDependencies": { "peerDependencies": {
"react": ">=19", "react": ">=19",
@@ -209,6 +214,11 @@
"import": "./dist/adapter/cloudflare/proxy.js", "import": "./dist/adapter/cloudflare/proxy.js",
"require": "./dist/adapter/cloudflare/proxy.js" "require": "./dist/adapter/cloudflare/proxy.js"
}, },
"./adapter/cloudflare/vite": {
"types": "./dist/types/adapter/cloudflare/vite.d.ts",
"import": "./dist/adapter/cloudflare/vite.js",
"require": "./dist/adapter/cloudflare/vite.js"
},
"./adapter": { "./adapter": {
"types": "./dist/types/adapter/index.d.ts", "types": "./dist/types/adapter/index.d.ts",
"import": "./dist/adapter/index.js" "import": "./dist/adapter/index.js"
+3 -2
View File
@@ -5,7 +5,6 @@ import type { em as prototypeEm } from "data/prototype";
import { Connection } from "data/connection/Connection"; import { Connection } from "data/connection/Connection";
import type { Hono } from "hono"; import type { Hono } from "hono";
import { import {
type InitialModuleConfigs,
type ModuleConfigs, type ModuleConfigs,
type Modules, type Modules,
ModuleManager, ModuleManager,
@@ -381,8 +380,10 @@ export class App<
if (results.length > 0) { if (results.length > 0) {
for (const { name, result } of results) { for (const { name, result } of results) {
if (result) { if (result) {
$console.log(`[Plugin:${name}] schema`);
ctx.helper.ensureSchema(result); ctx.helper.ensureSchema(result);
if (ctx.flags.sync_required) {
$console.log(`[Plugin:${name}] schema, sync required`);
}
} }
} }
} }
+3 -5
View File
@@ -1,14 +1,12 @@
/// <reference types="bun-types" />
import path from "node:path"; import path from "node:path";
import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter"; import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter";
import { registerLocalMediaAdapter } from "."; import { registerLocalMediaAdapter } from ".";
import { config, type App } from "bknd"; import { config, type App } from "bknd";
import type { ServeOptions } from "bun";
import { serveStatic } from "hono/bun"; import { serveStatic } from "hono/bun";
type BunEnv = Bun.Env; type BunEnv = Bun.Env;
export type BunBkndConfig<Env = BunEnv> = RuntimeBkndConfig<Env> & Omit<ServeOptions, "fetch">; export type BunBkndConfig<Env = BunEnv> = RuntimeBkndConfig<Env> &
Omit<Bun.Serve.Options<undefined, string>, "fetch">;
export async function createApp<Env = BunEnv>( export async function createApp<Env = BunEnv>(
{ distPath, serveStatic: _serveStatic, ...config }: BunBkndConfig<Env> = {}, { distPath, serveStatic: _serveStatic, ...config }: BunBkndConfig<Env> = {},
@@ -60,7 +58,7 @@ export function serve<Env = BunEnv>(
args: Env = Bun.env as Env, args: Env = Bun.env as Env,
) { ) {
Bun.serve({ Bun.serve({
...serveOptions, ...(serveOptions as any),
port, port,
fetch: createHandler( fetch: createHandler(
{ {
+29 -12
View File
@@ -1,5 +1,3 @@
import { inspect } from "node:util";
export type BindingTypeMap = { export type BindingTypeMap = {
D1Database: D1Database; D1Database: D1Database;
KVNamespace: KVNamespace; KVNamespace: KVNamespace;
@@ -10,21 +8,40 @@ export type BindingTypeMap = {
export type GetBindingType = keyof BindingTypeMap; export type GetBindingType = keyof BindingTypeMap;
export type BindingMap<T extends GetBindingType> = { key: string; value: BindingTypeMap[T] }; export type BindingMap<T extends GetBindingType> = { key: string; value: BindingTypeMap[T] };
export const bindingMatchers = {
// D1Database instance doesn't stringify to [object D1Database] (yet)
D1Database: (value: object): value is D1Database => {
if (typeof value !== "object" || value === null) return false;
if (String(value) === "[object D1Database]") return true;
if (Object.keys(value).length !== 2) return false;
return ["alwaysPrimarySession", "fetcher"].every((key) => key in value);
},
KVNamespace: (value: object): value is KVNamespace =>
String(value) === "[object KvNamespace]" && Object.keys(value).length === 0,
DurableObjectNamespace: (value: object): value is DurableObjectNamespace =>
String(value) === "[object DurableObjectNamespace]" && Object.keys(value).length === 0,
R2Bucket: (value: object): value is R2Bucket =>
String(value) === "[object R2Bucket]" && Object.keys(value).length === 0,
};
export function getBindings<T extends GetBindingType>(env: any, type: T): BindingMap<T>[] { export function getBindings<T extends GetBindingType>(env: any, type: T): BindingMap<T>[] {
const bindings: BindingMap<T>[] = []; const bindings: BindingMap<T>[] = [];
if (!(type in bindingMatchers)) {
console.error(`No binding matcher for type ${type}`);
return [];
}
const matcher = bindingMatchers[type];
for (const key in env) { for (const key in env) {
try { const value = env[key];
if ( if (typeof value !== "object" || value === null) continue;
(env[key] as any).constructor.name === type || if (!matcher(value)) continue;
String(env[key]) === `[object ${type}]` ||
inspect(env[key]).includes(type)
) {
bindings.push({ bindings.push({
key, key,
value: env[key] as BindingTypeMap[T], value,
}); } as any);
}
} catch (e) {}
} }
return bindings; return bindings;
} }
@@ -0,0 +1,51 @@
import { describe, afterAll, test, expect, beforeAll } from "vitest";
import { Miniflare } from "miniflare";
import { getBindings, type GetBindingType } from "./bindings";
let mf: Miniflare | undefined;
beforeAll(() => {
mf = new Miniflare({
modules: true,
script: `import { DurableObject } from "cloudflare:workers";
export default { async fetch() { return new Response(null); } }
export class TestObject extends DurableObject {}
export class TestObject2 extends DurableObject {}`,
r2Buckets: ["BUCKET"],
d1Databases: ["DB"],
kvNamespaces: ["KV"],
durableObjects: {
DO_SQLITE: { className: "TestObject", useSQLite: true },
DO_KV: { className: "TestObject2" },
},
});
});
afterAll(() => {
mf?.dispose();
});
describe("bindings", () => {
test("extracts", async () => {
const env = {
...(await mf!.getBindings()),
some: "string",
another: 123,
array: [1, 2, 3],
};
const $getBindingKeys = (type: GetBindingType) => {
const bindings = getBindings(env, type);
const keys = bindings?.map((b) => b.key);
if (!keys || keys.length === 0) {
console.error(`No ${type} binding found`, { bindings });
throw new Error(`No ${type} binding found`);
}
return keys;
};
expect($getBindingKeys("D1Database")).toEqual(["DB"]);
expect($getBindingKeys("KVNamespace")).toEqual(["KV"]);
expect($getBindingKeys("DurableObjectNamespace")).toEqual(["DO_SQLITE", "DO_KV"]);
expect($getBindingKeys("R2Bucket")).toEqual(["BUCKET"]);
});
});
+6 -2
View File
@@ -16,10 +16,14 @@ export {
type GetBindingType, type GetBindingType,
type BindingMap, type BindingMap,
} from "./bindings"; } from "./bindings";
export { constants, makeConfig, type CloudflareContext } from "./config"; export {
constants,
makeConfig,
type CloudflareContext,
registerAsyncsExecutionContext,
} from "./config";
export { StorageR2Adapter, registerMedia } from "./storage/StorageR2Adapter"; export { StorageR2Adapter, registerMedia } from "./storage/StorageR2Adapter";
export { registries } from "bknd"; export { registries } from "bknd";
export { devFsVitePlugin, devFsWrite } from "./vite";
// for compatibility with old code // for compatibility with old code
export function d1<DB extends D1Database | D1DatabaseSession = D1Database>( export function d1<DB extends D1Database | D1DatabaseSession = D1Database>(
@@ -16,6 +16,7 @@ export function registerMedia(
env: Record<string, any>, env: Record<string, any>,
registries: typeof $registries = $registries, registries: typeof $registries = $registries,
) { ) {
if (registries.media.has("r2")) return;
const r2_bindings = getBindings(env, "R2Bucket"); const r2_bindings = getBindings(env, "R2Bucket");
registries.media.register( registries.media.register(
@@ -172,7 +173,7 @@ export class StorageR2Adapter extends StorageAdapter {
await this.bucket.delete(this.getKey(key)); await this.bucket.delete(this.getKey(key));
} }
getObjectUrl(key: string): string { getObjectUrl(): string {
throw new Error("Method getObjectUrl not implemented."); throw new Error("Method getObjectUrl not implemented.");
} }
@@ -183,7 +184,7 @@ export class StorageR2Adapter extends StorageAdapter {
return key; return key;
} }
toJSON(secrets?: boolean) { toJSON() {
return { return {
type: this.getName(), type: this.getName(),
config: {}, config: {},
+1 -2
View File
@@ -1,3 +1,4 @@
// biome-ignore-all lint: .
import type { Plugin } from "vite"; import type { Plugin } from "vite";
import { writeFile as nodeWriteFile } from "node:fs/promises"; import { writeFile as nodeWriteFile } from "node:fs/promises";
import { resolve } from "node:path"; import { resolve } from "node:path";
@@ -48,7 +49,6 @@ export function devFsVitePlugin({
// Skip our own debug output // Skip our own debug output
if (output.includes("[dev-fs-plugin]") || output.includes("[dev-fs-polyfill]")) { if (output.includes("[dev-fs-plugin]") || output.includes("[dev-fs-polyfill]")) {
// @ts-ignore // @ts-ignore
// biome-ignore lint/style/noArguments: <explanation>
return originalStdoutWrite.apply(process.stdout, arguments); return originalStdoutWrite.apply(process.stdout, arguments);
} }
@@ -178,7 +178,6 @@ export function devFsVitePlugin({
} }
// @ts-ignore // @ts-ignore
// biome-ignore lint:
return originalStdoutWrite.apply(process.stdout, arguments); return originalStdoutWrite.apply(process.stdout, arguments);
}; };
+4 -1
View File
@@ -1,5 +1,6 @@
import { createFrameworkApp, type FrameworkBkndConfig } from "bknd/adapter"; import { createFrameworkApp, type FrameworkBkndConfig } from "bknd/adapter";
import { isNode } from "bknd/utils"; import { isNode } from "bknd/utils";
// @ts-expect-error next is not installed
import type { NextApiRequest } from "next"; import type { NextApiRequest } from "next";
type NextjsEnv = NextApiRequest["env"]; type NextjsEnv = NextApiRequest["env"];
@@ -18,7 +19,9 @@ function getCleanRequest(req: Request, cleanRequest: NextjsBkndConfig["cleanRequ
if (!cleanRequest) return req; if (!cleanRequest) return req;
const url = new URL(req.url); const url = new URL(req.url);
cleanRequest?.searchParams?.forEach((k) => url.searchParams.delete(k)); cleanRequest?.searchParams?.forEach((k) => {
url.searchParams.delete(k);
});
if (isNode()) { if (isNode()) {
return new Request(url.toString(), { return new Request(url.toString(), {
+1 -1
View File
@@ -4,7 +4,7 @@ import { resendEmail } from "./resend";
const ALL_TESTS = !!process.env.ALL_TESTS; const ALL_TESTS = !!process.env.ALL_TESTS;
describe.skipIf(ALL_TESTS)("resend", () => { describe.skipIf(ALL_TESTS)("resend", () => {
it.only("should throw on failed", async () => { it("should throw on failed", async () => {
const driver = resendEmail({ apiKey: "invalid" } as any); const driver = resendEmail({ apiKey: "invalid" } as any);
expect(driver.send("foo@bar.com", "Test", "Test")).rejects.toThrow(); expect(driver.send("foo@bar.com", "Test", "Test")).rejects.toThrow();
}); });
+2 -1
View File
@@ -73,8 +73,9 @@ export const env = <
onValid?: (valid: R) => void; onValid?: (valid: R) => void;
}, },
): R extends undefined ? Fallback : R => { ): R extends undefined ? Fallback : R => {
const source = opts?.source ?? (typeof process !== "undefined" ? process?.env : {});
try { try {
const source = opts?.source ?? process.env;
const c = envs[key]; const c = envs[key];
const g = source[c.key]; const g = source[c.key];
const v = c.validate(g) as any; const v = c.validate(g) as any;
+2 -5
View File
@@ -120,17 +120,14 @@ export function patternMatch(target: string, pattern: RegExp | string): boolean
} }
export function slugify(str: string): string { export function slugify(str: string): string {
return ( return String(str)
String(str)
.normalize("NFKD") // split accented characters into their base characters and diacritical marks .normalize("NFKD") // split accented characters into their base characters and diacritical marks
// biome-ignore lint/suspicious/noMisleadingCharacterClass: <explanation>
.replace(/[\u0300-\u036f]/g, "") // remove all the accents, which happen to be all in the \u03xx UNICODE block. .replace(/[\u0300-\u036f]/g, "") // remove all the accents, which happen to be all in the \u03xx UNICODE block.
.trim() // trim leading or trailing whitespace .trim() // trim leading or trailing whitespace
.toLowerCase() // convert to lowercase .toLowerCase() // convert to lowercase
.replace(/[^a-z0-9 -]/g, "") // remove non-alphanumeric characters .replace(/[^a-z0-9 -]/g, "") // remove non-alphanumeric characters
.replace(/\s+/g, "-") // replace spaces with hyphens .replace(/\s+/g, "-") // replace spaces with hyphens
.replace(/-+/g, "-") // remove consecutive hyphens .replace(/-+/g, "-"); // remove consecutive hyphens
);
} }
export function truncate(str: string, length = 50, end = "..."): string { export function truncate(str: string, length = 50, end = "..."): string {
+4 -9
View File
@@ -17,6 +17,7 @@ import {
type Simplify, type Simplify,
sql, sql,
} from "kysely"; } from "kysely";
import type { jsonArrayFrom, jsonBuildObject, jsonObjectFrom } from "kysely/helpers/sqlite";
import type { BaseIntrospector, BaseIntrospectorConfig } from "./BaseIntrospector"; import type { BaseIntrospector, BaseIntrospectorConfig } from "./BaseIntrospector";
import type { DB } from "bknd"; import type { DB } from "bknd";
import type { Constructor } from "core/registry/Registry"; import type { Constructor } from "core/registry/Registry";
@@ -70,15 +71,9 @@ export type IndexSpec = {
}; };
export type DbFunctions = { export type DbFunctions = {
jsonObjectFrom<O>(expr: SelectQueryBuilderExpression<O>): RawBuilder<Simplify<O> | null>; jsonObjectFrom: typeof jsonObjectFrom;
jsonArrayFrom<O>(expr: SelectQueryBuilderExpression<O>): RawBuilder<Simplify<O>[]>; jsonArrayFrom: typeof jsonArrayFrom;
jsonBuildObject<O extends Record<string, Expression<unknown>>>( jsonBuildObject: typeof jsonBuildObject;
obj: O,
): RawBuilder<
Simplify<{
[K in keyof O]: O[K] extends Expression<infer V> ? V : never;
}>
>;
}; };
export type ConnQuery = CompiledQuery | Compilable; export type ConnQuery = CompiledQuery | Compilable;
@@ -18,7 +18,7 @@ export type LibsqlClientFns = {
function getClient(clientOrCredentials: Client | LibSqlCredentials | LibsqlClientFns): Client { function getClient(clientOrCredentials: Client | LibSqlCredentials | LibsqlClientFns): Client {
if (clientOrCredentials && "url" in clientOrCredentials) { if (clientOrCredentials && "url" in clientOrCredentials) {
const { url, authToken } = clientOrCredentials; const { url, authToken } = clientOrCredentials;
return createClient({ url, authToken }); return createClient({ url, authToken }) as unknown as Client;
} }
return clientOrCredentials as Client; return clientOrCredentials as Client;
+7 -15
View File
@@ -136,10 +136,8 @@ export class Entity<
.map((field) => (alias ? `${alias}.${field.name} as ${field.name}` : field.name)); .map((field) => (alias ? `${alias}.${field.name} as ${field.name}` : field.name));
} }
getFillableFields(context?: "create" | "update", include_virtual?: boolean): Field[] { getFillableFields(context?: TActionContext, include_virtual?: boolean): Field[] {
return this.getFields({ virtual: include_virtual }).filter((field) => return this.getFields(include_virtual).filter((field) => field.isFillable(context));
field.isFillable(context),
);
} }
getRequiredFields(): Field[] { getRequiredFields(): Field[] {
@@ -191,15 +189,9 @@ export class Entity<
return this.fields.findIndex((field) => field.name === name) !== -1; return this.fields.findIndex((field) => field.name === name) !== -1;
} }
getFields({ getFields(include_virtual: boolean = false): Field[] {
virtual = false, if (include_virtual) return this.fields;
primary = true, return this.fields.filter((f) => !f.isVirtual());
}: { virtual?: boolean; primary?: boolean } = {}): Field[] {
return this.fields.filter((f) => {
if (!virtual && f.isVirtual()) return false;
if (!primary && f instanceof PrimaryField) return false;
return true;
});
} }
addField(field: Field) { addField(field: Field) {
@@ -239,7 +231,7 @@ export class Entity<
} }
} }
const fields = this.getFillableFields(context as any, false); const fields = this.getFillableFields(context, false);
if (options?.ignoreUnknown !== true) { if (options?.ignoreUnknown !== true) {
const field_names = fields.map((f) => f.name); const field_names = fields.map((f) => f.name);
@@ -283,7 +275,7 @@ export class Entity<
fields = this.getFillableFields(options.context); fields = this.getFillableFields(options.context);
break; break;
default: default:
fields = this.getFields({ virtual: true }); fields = this.getFields(true);
} }
const _fields = Object.fromEntries(fields.map((field) => [field.name, field])); const _fields = Object.fromEntries(fields.map((field) => [field.name, field]));
+2 -4
View File
@@ -83,10 +83,8 @@ export class Mutator<
} }
// we should never get here, but just to be sure (why?) // we should never get here, but just to be sure (why?)
if (!field.isFillable(context as any)) { if (!field.isFillable(context)) {
throw new Error( throw new Error(`Field "${key}" is not fillable on entity "${entity.name}"`);
`Field "${key}" of entity "${entity.name}" is not fillable on context "${context}"`,
);
} }
// transform from field // transform from field
+6 -14
View File
@@ -26,19 +26,11 @@ export const baseFieldConfigSchema = s
.strictObject({ .strictObject({
label: s.string(), label: s.string(),
description: s.string(), description: s.string(),
required: s.boolean({ default: DEFAULT_REQUIRED }), required: s.boolean({ default: false }),
fillable: s.anyOf( fillable: s.anyOf([
[
s.boolean({ title: "Boolean" }), s.boolean({ title: "Boolean" }),
s.array(s.string({ enum: ["create", "update"] }), { s.array(s.string({ enum: ActionContext }), { title: "Context", uniqueItems: true }),
title: "Context", ]),
uniqueItems: true,
}),
],
{
default: DEFAULT_FILLABLE,
},
),
hidden: s.anyOf([ hidden: s.anyOf([
s.boolean({ title: "Boolean" }), s.boolean({ title: "Boolean" }),
// @todo: tmp workaround // @todo: tmp workaround
@@ -111,7 +103,7 @@ export abstract class Field<
return this.config?.default_value; return this.config?.default_value;
} }
isFillable(context?: "create" | "update"): boolean { isFillable(context?: TActionContext): boolean {
if (Array.isArray(this.config.fillable)) { if (Array.isArray(this.config.fillable)) {
return context ? this.config.fillable.includes(context) : DEFAULT_FILLABLE; return context ? this.config.fillable.includes(context) : DEFAULT_FILLABLE;
} }
@@ -173,7 +165,7 @@ export abstract class Field<
// @todo: add field level validation // @todo: add field level validation
isValid(value: any, context: TActionContext): boolean { isValid(value: any, context: TActionContext): boolean {
if (typeof value !== "undefined") { if (typeof value !== "undefined") {
return this.isFillable(context as any); return this.isFillable(context);
} else if (context === "create") { } else if (context === "create") {
return !this.isRequired(); return !this.isRequired();
} }
+5 -2
View File
@@ -99,7 +99,6 @@ export function fieldTestSuite(
const _config = { const _config = {
..._requiredConfig, ..._requiredConfig,
required: false, required: false,
fillable: true,
}; };
function fieldJson(field: Field) { function fieldJson(field: Field) {
@@ -117,7 +116,10 @@ export function fieldTestSuite(
expect(fieldJson(fillable)).toEqual({ expect(fieldJson(fillable)).toEqual({
type: noConfigField.type, type: noConfigField.type,
config: _config, config: {
..._config,
fillable: true,
},
}); });
expect(fieldJson(required)).toEqual({ expect(fieldJson(required)).toEqual({
@@ -148,6 +150,7 @@ export function fieldTestSuite(
type: requiredAndDefault.type, type: requiredAndDefault.type,
config: { config: {
..._config, ..._config,
fillable: true,
required: true, required: true,
default_value: config.defaultValue, default_value: config.defaultValue,
}, },
+1 -1
View File
@@ -77,7 +77,7 @@ export class SchemaManager {
} }
getIntrospectionFromEntity(entity: Entity): IntrospectedTable { getIntrospectionFromEntity(entity: Entity): IntrospectedTable {
const fields = entity.getFields({ virtual: false }); const fields = entity.getFields(false);
const indices = this.em.getIndicesOf(entity); const indices = this.em.getIndicesOf(entity);
// this is intentionally setting values to defaults, like "nullable" and "default" // this is intentionally setting values to defaults, like "nullable" and "default"
+11 -13
View File
@@ -23,26 +23,17 @@ export type HybridMode<AdapterConfig extends BkndConfig> = AdapterConfig extends
? BkndModeConfig<Args, Merge<BkndHybridModeOptions & AdapterConfig>> ? BkndModeConfig<Args, Merge<BkndHybridModeOptions & AdapterConfig>>
: never; : never;
export function hybrid<Args>({ export function hybrid<Args>(hybridConfig: HybridBkndConfig<Args>): BkndConfig<Args> {
configFilePath = "bknd-config.json",
...rest
}: HybridBkndConfig<Args>): BkndConfig<Args> {
return { return {
...rest,
config: undefined,
app: async (args) => { app: async (args) => {
const { const {
config: appConfig, config: appConfig,
isProd, isProd,
plugins, plugins,
syncSchemaOptions, syncSchemaOptions,
} = await makeModeConfig( } = await makeModeConfig(hybridConfig, args);
{
...rest, const configFilePath = appConfig.configFilePath ?? "bknd-config.json";
configFilePath,
},
args,
);
if (appConfig?.options?.mode && appConfig?.options?.mode !== "db") { if (appConfig?.options?.mode && appConfig?.options?.mode !== "db") {
$console.warn("You should not set a different mode than `db` when using hybrid mode"); $console.warn("You should not set a different mode than `db` when using hybrid mode");
@@ -80,6 +71,13 @@ export function hybrid<Args>({
skipValidation: isProd, skipValidation: isProd,
// secrets are required for hybrid mode // secrets are required for hybrid mode
secrets: appConfig.secrets, secrets: appConfig.secrets,
onModulesBuilt: async (ctx) => {
if (ctx.flags.sync_required && !isProd && syncSchemaOptions.force) {
$console.log("[hybrid] syncing schema");
await ctx.em.schema().sync(syncSchemaOptions);
}
await appConfig?.options?.manager?.onModulesBuilt?.(ctx);
},
...appConfig?.options?.manager, ...appConfig?.options?.manager,
}, },
}, },
+1 -1
View File
@@ -1,4 +1,4 @@
import { mark, stripMark, $console, s, SecretSchema, setPath } from "bknd/utils"; import { mark, stripMark, $console, s, setPath } from "bknd/utils";
import { BkndError } from "core/errors"; import { BkndError } from "core/errors";
import * as $diff from "core/object/diff"; import * as $diff from "core/object/diff";
import type { Connection } from "data/connection"; import type { Connection } from "data/connection";
-24
View File
@@ -1,7 +1,6 @@
import { transformObject } from "bknd/utils"; import { transformObject } from "bknd/utils";
import type { Kysely } from "kysely"; import type { Kysely } from "kysely";
import { set } from "lodash-es"; import { set } from "lodash-es";
import type { InitialModuleConfigs } from "modules/ModuleManager";
export type MigrationContext = { export type MigrationContext = {
db: Kysely<any>; db: Kysely<any>;
@@ -108,29 +107,6 @@ export const migrations: Migration[] = [
return config; return config;
}, },
}, },
{
// change field.config.fillable to only "create" and "update"
version: 11,
up: async (config: InitialModuleConfigs) => {
const { data, ...rest } = config;
return {
...rest,
data: {
...data,
entities: transformObject(data?.entities ?? {}, (entity) => {
return {
...entity,
fields: transformObject(entity?.fields ?? {}, (field) => {
const fillable = field!.config?.fillable;
if (!fillable || typeof fillable === "boolean") return field;
return { ...field, config: { ...field!.config, fillable: true } };
}),
};
}),
},
};
},
},
]; ];
export const CURRENT_VERSION = migrations[migrations.length - 1]?.version ?? 0; export const CURRENT_VERSION = migrations[migrations.length - 1]?.version ?? 0;
@@ -30,7 +30,7 @@ export const Group = <E extends ElementType = "div">({
{...props} {...props}
data-role="group" data-role="group"
className={twMerge( className={twMerge(
"flex flex-col gap-1.5 has-disabled:cursor-not-allowed w-full", "flex flex-col gap-1.5 w-full",
as === "fieldset" && "border border-primary/10 p-3 rounded-md", as === "fieldset" && "border border-primary/10 p-3 rounded-md",
as === "fieldset" && error && "border-red-500", as === "fieldset" && error && "border-red-500",
error && "text-red-500", error && "text-red-500",
@@ -97,7 +97,7 @@ export const Input = forwardRef<HTMLInputElement, React.ComponentProps<"input">>
ref={ref} ref={ref}
className={twMerge( className={twMerge(
"bg-muted/40 h-11 rounded-md py-2.5 px-4 outline-none w-full disabled:cursor-not-allowed", "bg-muted/40 h-11 rounded-md py-2.5 px-4 outline-none w-full disabled:cursor-not-allowed",
disabledOrReadonly && "bg-muted/50 text-primary/50 cursor-not-allowed", disabledOrReadonly && "bg-muted/50 text-primary/50",
!disabledOrReadonly && !disabledOrReadonly &&
"focus:bg-muted focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all", "focus:bg-muted focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all",
props.className, props.className,
@@ -154,7 +154,7 @@ export const Textarea = forwardRef<HTMLTextAreaElement, React.ComponentProps<"te
{...props} {...props}
ref={ref} ref={ref}
className={twMerge( className={twMerge(
"bg-muted/40 min-h-11 rounded-md py-2.5 px-4 focus:bg-muted outline-none focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all disabled:bg-muted/50 disabled:text-primary/50 disabled:cursor-not-allowed", "bg-muted/40 min-h-11 rounded-md py-2.5 px-4 focus:bg-muted outline-none focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all disabled:bg-muted/50 disabled:text-primary/50",
props.className, props.className,
)} )}
/> />
@@ -214,7 +214,7 @@ export const BooleanInput = forwardRef<HTMLInputElement, React.ComponentProps<"i
{...props} {...props}
type="checkbox" type="checkbox"
ref={ref} ref={ref}
className="outline-none focus:outline-none focus:ring-2 focus:ring-zinc-500 transition-all disabled:opacity-70 disabled:cursor-not-allowed scale-150 ml-1" className="outline-none focus:outline-none focus:ring-2 focus:ring-zinc-500 transition-all disabled:opacity-70 scale-150 ml-1"
checked={checked} checked={checked}
onChange={handleCheck} onChange={handleCheck}
disabled={props.disabled} disabled={props.disabled}
@@ -260,6 +260,7 @@ export const Switch = forwardRef<
props.disabled && "opacity-50 !cursor-not-allowed", props.disabled && "opacity-50 !cursor-not-allowed",
)} )}
onCheckedChange={(bool) => { onCheckedChange={(bool) => {
console.log("setting", bool);
props.onChange?.({ target: { value: bool } }); props.onChange?.({ target: { value: bool } });
}} }}
{...(props as any)} {...(props as any)}
@@ -293,7 +294,7 @@ export const Select = forwardRef<
{...props} {...props}
ref={ref} ref={ref}
className={twMerge( className={twMerge(
"bg-muted/40 focus:bg-muted rounded-md py-2.5 px-4 outline-none focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all disabled:bg-muted/50 disabled:text-primary/50 disabled:cursor-not-allowed", "bg-muted/40 focus:bg-muted rounded-md py-2.5 px-4 outline-none focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all disabled:bg-muted/50 disabled:text-primary/50",
"appearance-none h-11 w-full", "appearance-none h-11 w-full",
!props.multiple && "border-r-8 border-r-transparent", !props.multiple && "border-r-8 border-r-transparent",
props.className, props.className,
@@ -19,9 +19,10 @@ import type { RelationField } from "data/relations";
import { useEntityAdminOptions } from "ui/options"; import { useEntityAdminOptions } from "ui/options";
// simplify react form types 🤦 // simplify react form types 🤦
export type FormApi = ReactFormExtendedApi<any, any, any, any, any, any, any, any, any, any>;
// biome-ignore format: ... // biome-ignore format: ...
export type TFieldApi = FieldApi<any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any>; export type FormApi = ReactFormExtendedApi<any, any, any, any, any, any, any, any, any, any, any, any>;
// biome-ignore format: ...
export type TFieldApi = FieldApi<any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any>;
type EntityFormProps = { type EntityFormProps = {
entity: Entity; entity: Entity;
@@ -44,7 +45,7 @@ export function EntityForm({
className, className,
action, action,
}: EntityFormProps) { }: EntityFormProps) {
const fields = entity.getFields({ virtual: true, primary: false }); const fields = entity.getFillableFields(action, true);
const options = useEntityAdminOptions(entity, action); const options = useEntityAdminOptions(entity, action);
return ( return (
@@ -92,6 +93,10 @@ export function EntityForm({
); );
} }
if (!field.isFillable(action)) {
return;
}
const _key = `${entity.name}-${field.name}-${key}`; const _key = `${entity.name}-${field.name}-${key}`;
return ( return (
@@ -123,7 +128,7 @@ export function EntityForm({
<EntityFormField <EntityFormField
field={field} field={field}
fieldApi={props} fieldApi={props}
disabled={fieldsDisabled || !field.isFillable(action)} disabled={fieldsDisabled}
tabIndex={key + 1} tabIndex={key + 1}
action={action} action={action}
data={data} data={data}
@@ -17,7 +17,7 @@ export function useEntityForm({
}: EntityFormProps) { }: EntityFormProps) {
const data = initialData ?? {}; const data = initialData ?? {};
// @todo: check if virtual must be filtered // @todo: check if virtual must be filtered
const fields = entity.getFields({ virtual: true, primary: false }); const fields = entity.getFillableFields(action, true);
// filter defaultValues to only contain fillable fields // filter defaultValues to only contain fillable fields
const defaultValues = getDefaultValues(fields, data); const defaultValues = getDefaultValues(fields, data);
+1 -1
View File
@@ -207,7 +207,7 @@ function DataEntityUpdateImpl({ params }) {
handleSubmit={handleSubmit} handleSubmit={handleSubmit}
fieldsDisabled={fieldsDisabled} fieldsDisabled={fieldsDisabled}
data={data ?? undefined} data={data ?? undefined}
Form={Form} Form={Form as any}
action="update" action="update"
className="flex flex-grow flex-col gap-3 p-3" className="flex flex-grow flex-col gap-3 p-3"
/> />
@@ -121,7 +121,7 @@ export function DataEntityCreate({ params }) {
handleSubmit={handleSubmit} handleSubmit={handleSubmit}
fieldsDisabled={fieldsDisabled} fieldsDisabled={fieldsDisabled}
data={search.value} data={search.value}
Form={Form} Form={Form as any}
action="create" action="create"
className="flex flex-grow flex-col gap-3 p-3" className="flex flex-grow flex-col gap-3 p-3"
/> />
+2 -2
View File
@@ -1,4 +1,4 @@
import type { JSONSchema7 } from "json-schema"; import type { JSONSchema } from "json-schema-to-ts";
import { omitKeys, type s } from "bknd/utils"; import { omitKeys, type s } from "bknd/utils";
export function extractSchema< export function extractSchema<
@@ -10,7 +10,7 @@ export function extractSchema<
config: Config, config: Config,
keys: Keys[], keys: Keys[],
): [ ): [
JSONSchema7, Exclude<JSONSchema, boolean | null | undefined>,
Partial<Config>, Partial<Config>,
{ {
[K in Keys]: { [K in Keys]: {
+1 -1
View File
@@ -5,7 +5,7 @@ import path from "node:path";
export default defineConfig({ export default defineConfig({
plugins: [tsconfigPaths()], plugins: [tsconfigPaths()],
test: { test: {
projects: ["**/*.vitest.config.ts"], projects: ["**/*.vitest.config.ts", "**/*/vitest.config.ts"],
include: ["**/*.vi-test.ts", "**/*.vitest.ts"], include: ["**/*.vi-test.ts", "**/*.vitest.ts"],
}, },
}); });
+29 -22
View File
@@ -1,14 +1,13 @@
{ {
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", "$schema": "https://biomejs.dev/schemas/2.3.3/schema.json",
"organizeImports": { "assist": { "actions": { "source": { "organizeImports": "off" } } },
"enabled": true
},
"vcs": { "vcs": {
"defaultBranch": "main" "defaultBranch": "main"
}, },
"formatter": { "formatter": {
"enabled": true, "enabled": true,
"indentStyle": "space" "indentStyle": "space",
"formatWithErrors": true
}, },
"javascript": { "javascript": {
"formatter": { "formatter": {
@@ -30,32 +29,36 @@
} }
}, },
"files": { "files": {
"ignore": [ "includes": [
"**/node_modules/**", "**",
"node_modules/**", "!!**/node_modules",
"**/.cache/**", "!!**/.cache",
"**/.wrangler/**", "!!**/.wrangler",
"**/build/**", "!!**/build",
"**/dist/**", "!!**/dist",
"**/data.sqld/**", "!!**/data.sqld",
"data.sqld/**", "!!**/data.sqld",
"public/**", "!!**/public",
".history/**" "!!**/.history"
] ]
}, },
"linter": { "linter": {
"enabled": true, "enabled": true,
"ignore": ["**/*.spec.ts"], "includes": ["**"],
"rules": { "rules": {
"recommended": true, "recommended": true,
"a11y": { "a11y": {},
"all": false
},
"correctness": { "correctness": {
"useExhaustiveDependencies": "off", "useExhaustiveDependencies": "off",
"noUnreachable": "warn", "noUnreachable": "warn",
"noChildrenProp": "off", "noChildrenProp": "off",
"noSwitchDeclarations": "warn" "noSwitchDeclarations": "warn",
"noUnusedVariables": {
"options": {
"ignoreRestSiblings": true
},
"level": "warn"
}
}, },
"complexity": { "complexity": {
"noUselessFragments": "warn", "noUselessFragments": "warn",
@@ -70,7 +73,11 @@
"noArrayIndexKey": "off", "noArrayIndexKey": "off",
"noImplicitAnyLet": "warn", "noImplicitAnyLet": "warn",
"noConfusingVoidType": "off", "noConfusingVoidType": "off",
"noConsoleLog": "warn" "noConsole": {
"level": "warn",
"options": { "allow": ["error", "info"] }
},
"noTsIgnore": "off"
}, },
"security": { "security": {
"noDangerouslySetInnerHtml": "off" "noDangerouslySetInnerHtml": "off"
+1003 -720
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
[install]
#linker = "hoisted"
@@ -164,7 +164,7 @@ The [Cloudflare Vite Plugin](https://developers.cloudflare.com/workers/vite-plug
To fix this, bknd exports a Vite plugin that provides filesystem access during development. You can use it by adding the following to your `vite.config.ts` file: To fix this, bknd exports a Vite plugin that provides filesystem access during development. You can use it by adding the following to your `vite.config.ts` file:
```ts ```ts
import { devFsVitePlugin } from "bknd/adapter/cloudflare"; import { devFsVitePlugin } from "bknd/adapter/cloudflare/vite";
export default defineConfig({ export default defineConfig({
plugins: [devFsVitePlugin()], // [!code highlight] plugins: [devFsVitePlugin()], // [!code highlight]
@@ -174,7 +174,7 @@ export default defineConfig({
Now to use this polyfill, you can use the `devFsWrite` function to write files to the filesystem. Now to use this polyfill, you can use the `devFsWrite` function to write files to the filesystem.
```ts ```ts
import { devFsWrite } from "bknd/adapter/cloudflare"; // [!code highlight] import { devFsWrite } from "bknd/adapter/cloudflare/vite"; // [!code highlight]
import { syncTypes } from "bknd/plugins"; import { syncTypes } from "bknd/plugins";
export default { export default {
+4 -4
View File
@@ -6,15 +6,15 @@
"scripts": { "scripts": {
"deploy": "wrangler deploy", "deploy": "wrangler deploy",
"dev": "wrangler dev", "dev": "wrangler dev",
"bknd-typegen": "PROXY=1 npx bknd types", "bknd-typegen": "PROXY=1 node_modules/.bin/bknd types",
"typegen": "wrangler types && npm run bknd-typegen", "typegen": "wrangler types && npm run bknd-typegen",
"predev": "npm run typegen" "postinstall": "npm run typegen"
}, },
"dependencies": { "dependencies": {
"bknd": "file:../../app" "bknd": "file:../../app"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.9.2", "typescript": "^5.9.3",
"wrangler": "^4.34.0" "wrangler": "^4.46.0"
} }
} }
+1 -2
View File
@@ -2,8 +2,7 @@
"$schema": "node_modules/wrangler/config-schema.json", "$schema": "node_modules/wrangler/config-schema.json",
"name": "bknd-cf-worker-example", "name": "bknd-cf-worker-example",
"main": "src/index.ts", "main": "src/index.ts",
"compatibility_date": "2025-08-03", "compatibility_date": "2025-11-05",
"compatibility_flags": ["nodejs_compat"],
"workers_dev": true, "workers_dev": true,
"minify": true, "minify": true,
"assets": { "assets": {
+6 -6
View File
@@ -12,13 +12,13 @@
}, },
"dependencies": {}, "dependencies": {},
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.9.4", "@biomejs/biome": "2.3.3",
"@tsconfig/strictest": "^2.0.5", "@tsconfig/strictest": "^2.0.7",
"@types/bun": "^1.3.1",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"bun-types": "^1.1.18", "miniflare": "^3.20250718.2",
"miniflare": "^3.20240806.0", "typescript": "^5.9.3",
"typescript": "^5.5.3", "verdaccio": "^6.2.1"
"verdaccio": "^5.32.1"
}, },
"engines": { "engines": {
"node": ">=22" "node": ">=22"