mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 16:16:02 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c26d646bd | |||
| 4859b99954 | |||
| 3f55c0f2c5 | |||
| 9ec31373e1 | |||
| 70b25a9f9b | |||
| 300d86ff7c | |||
| 4c9b662f6f | |||
| a862cfdcf1 | |||
| de62f4e729 | |||
| 4575d89cfe | |||
| 0ca0828581 | |||
| 07a57ebf67 | |||
| e9f1241ec3 | |||
| 80903d4ffa | |||
| c8290802e9 | |||
| ac6cd4a900 | |||
| a91ddb1ec3 |
@@ -20,7 +20,7 @@ jobs:
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: "1.2.22"
|
||||
bun-version: "1.3.1"
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./app
|
||||
|
||||
+2
-1
@@ -27,7 +27,8 @@ packages/media/.env
|
||||
.npmrc
|
||||
/.verdaccio
|
||||
.idea
|
||||
.vscode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
.git_old
|
||||
docker/tmp
|
||||
.debug
|
||||
|
||||
Vendored
+19
@@ -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"]
|
||||
}
|
||||
}
|
||||
Vendored
+12
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,7 @@ describe("App tests", async () => {
|
||||
expect(Array.from(app.plugins.keys())).toEqual(["test"]);
|
||||
});
|
||||
|
||||
test.only("drivers", async () => {
|
||||
test("drivers", async () => {
|
||||
const called: string[] = [];
|
||||
const app = new App(dummyConnection, undefined, {
|
||||
drivers: {
|
||||
|
||||
@@ -15,7 +15,7 @@ const mockedBackend = new Hono()
|
||||
.get("/file/:name", async (c) => {
|
||||
const { name } = c.req.param();
|
||||
const file = Bun.file(`${assetsPath}/${name}`);
|
||||
return new Response(file, {
|
||||
return new Response(new File([await file.bytes()], name, { type: file.type }), {
|
||||
headers: {
|
||||
"Content-Type": file.type,
|
||||
"Content-Length": file.size.toString(),
|
||||
|
||||
@@ -76,7 +76,7 @@ describe("repros", async () => {
|
||||
expect(app.em.entities.map((e) => e.name)).toEqual(["media", "test"]);
|
||||
});
|
||||
|
||||
test.only("verify inversedBy", async () => {
|
||||
test("verify inversedBy", async () => {
|
||||
const schema = proto.em(
|
||||
{
|
||||
products: proto.entity("products", {
|
||||
|
||||
@@ -7,9 +7,7 @@ import v7 from "./samples/v7.json";
|
||||
import v8 from "./samples/v8.json";
|
||||
import v8_2 from "./samples/v8-2.json";
|
||||
import v9 from "./samples/v9.json";
|
||||
import v10 from "./samples/v10.json";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
import { CURRENT_VERSION } from "modules/db/migrations";
|
||||
|
||||
beforeAll(() => disableConsoleLog());
|
||||
afterAll(enableConsoleLog);
|
||||
@@ -63,7 +61,7 @@ async function getRawConfig(
|
||||
return await db
|
||||
.selectFrom("__bknd")
|
||||
.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))
|
||||
.execute();
|
||||
}
|
||||
@@ -117,6 +115,7 @@ describe("Migrations", () => {
|
||||
"^^s3.secret_access_key^^",
|
||||
);
|
||||
const [config, secrets] = (await getRawConfig(app, {
|
||||
version: 10,
|
||||
types: ["config", "secrets"],
|
||||
})) as any;
|
||||
|
||||
@@ -130,15 +129,4 @@ describe("Migrations", () => {
|
||||
"^^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": {}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,10 @@
|
||||
import pkg from "./package.json" with { type: "json" };
|
||||
import c from "picocolors";
|
||||
import { formatNumber } from "bknd/utils";
|
||||
import * as esbuild from "esbuild";
|
||||
|
||||
const deps = Object.keys(pkg.dependencies);
|
||||
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({
|
||||
entrypoints: ["./src/cli/index.ts"],
|
||||
target: "node",
|
||||
|
||||
+13
-2
@@ -1,3 +1,4 @@
|
||||
/** biome-ignore-all lint/suspicious/noConsole: . */
|
||||
import { $ } from "bun";
|
||||
import * as tsup from "tsup";
|
||||
import pkg from "./package.json" with { type: "json" };
|
||||
@@ -96,6 +97,7 @@ async function buildApi() {
|
||||
metafile: true,
|
||||
target: "esnext",
|
||||
platform: "browser",
|
||||
removeNodeProtocol: false,
|
||||
format: ["esm"],
|
||||
splitting: false,
|
||||
loader: {
|
||||
@@ -228,6 +230,7 @@ function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsu
|
||||
outDir: `dist/adapter/${adapter}`,
|
||||
metafile: true,
|
||||
splitting: false,
|
||||
removeNodeProtocol: false,
|
||||
onSuccess: async () => {
|
||||
delayTypes();
|
||||
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("bun", {
|
||||
external: [/^bun\:.*/],
|
||||
external: [/^bun:.*/],
|
||||
}),
|
||||
),
|
||||
tsup.build(baseConfig("astro")),
|
||||
@@ -284,6 +287,14 @@ async function buildAdapters() {
|
||||
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({
|
||||
...baseConfig("vite"),
|
||||
@@ -320,7 +331,7 @@ async function buildAdapters() {
|
||||
entry: ["src/adapter/sqlite/bun.ts"],
|
||||
outDir: "dist/adapter/sqlite",
|
||||
metafile: false,
|
||||
external: [/^bun\:.*/],
|
||||
external: [/^bun:.*/],
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
[install]
|
||||
#registry = "http://localhost:4873"
|
||||
|
||||
[test]
|
||||
coverageSkipTestFiles = true
|
||||
console.depth = 10
|
||||
+59
-49
@@ -13,7 +13,7 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/bknd-io/bknd/issues"
|
||||
},
|
||||
"packageManager": "bun@1.2.22",
|
||||
"packageManager": "bun@1.3.1",
|
||||
"engines": {
|
||||
"node": ">=22.13"
|
||||
},
|
||||
@@ -49,93 +49,98 @@
|
||||
"license": "FSL-1.1-MIT",
|
||||
"dependencies": {
|
||||
"@cfworker/json-schema": "^4.1.1",
|
||||
"@codemirror/lang-html": "^6.4.9",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-html": "^6.4.11",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@hono/swagger-ui": "^0.5.1",
|
||||
"@hono/swagger-ui": "^0.5.2",
|
||||
"@mantine/core": "^7.17.1",
|
||||
"@mantine/hooks": "^7.17.1",
|
||||
"@tanstack/react-form": "^1.0.5",
|
||||
"@uiw/react-codemirror": "^4.23.10",
|
||||
"@xyflow/react": "^12.4.4",
|
||||
"@uiw/react-codemirror": "^4.25.2",
|
||||
"@xyflow/react": "^12.9.2",
|
||||
"aws4fetch": "^1.0.20",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"fast-xml-parser": "^5.0.8",
|
||||
"hono": "4.8.3",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"dayjs": "^1.11.19",
|
||||
"fast-xml-parser": "^5.3.1",
|
||||
"hono": "4.10.4",
|
||||
"json-schema-library": "10.0.0-rc7",
|
||||
"json-schema-to-ts": "^3.1.1",
|
||||
"jsonv-ts": "0.9.1",
|
||||
"kysely": "0.27.6",
|
||||
"jsonv-ts": "0.9.3",
|
||||
"kysely": "0.28.8",
|
||||
"lodash-es": "^4.17.21",
|
||||
"oauth4webapi": "^2.11.1",
|
||||
"object-path-immutable": "^4.1.2",
|
||||
"radix-ui": "^1.1.3",
|
||||
"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": {
|
||||
"@aws-sdk/client-s3": "^3.758.0",
|
||||
"@bluwy/giget-core": "^0.1.2",
|
||||
"@aws-sdk/client-s3": "^3.922.0",
|
||||
"@bluwy/giget-core": "^0.1.6",
|
||||
"@clack/prompts": "^0.11.0",
|
||||
"@cloudflare/vitest-pool-workers": "^0.9.3",
|
||||
"@cloudflare/workers-types": "^4.20250606.0",
|
||||
"@cloudflare/vitest-pool-workers": "^0.10.4",
|
||||
"@cloudflare/workers-types": "^4.20251014.0",
|
||||
"@dagrejs/dagre": "^1.1.4",
|
||||
"@hono/vite-dev-server": "^0.21.0",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@libsql/client": "^0.15.9",
|
||||
"@hono/vite-dev-server": "^0.23.0",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@libsql/client": "^0.15.15",
|
||||
"@mantine/modals": "^7.17.1",
|
||||
"@mantine/notifications": "^7.17.1",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@playwright/test": "^1.56.1",
|
||||
"@rjsf/core": "5.22.2",
|
||||
"@rjsf/utils": "5.22.0",
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@tabler/icons-react": "3.18.0",
|
||||
"@tailwindcss/postcss": "^4.0.12",
|
||||
"@tailwindcss/vite": "^4.0.12",
|
||||
"@tabler/icons-react": "3.35.0",
|
||||
"@tailwindcss/postcss": "^4.1.16",
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
"@tanstack/react-store": "^0.8.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@types/node": "^22.13.10",
|
||||
"@types/node": "^24.10.0",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@vitest/coverage-v8": "^3.0.9",
|
||||
"@vitejs/plugin-react": "^5.1.0",
|
||||
"@vitest/coverage-v8": "3.0.9",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"clsx": "^2.1.1",
|
||||
"dotenv": "^16.4.7",
|
||||
"commander": "^14.0.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"jotai": "^2.12.2",
|
||||
"jsdom": "^26.0.0",
|
||||
"kysely-d1": "^0.3.0",
|
||||
"jsdom": "^26.1.0",
|
||||
"kysely-generic-sqlite": "^1.2.1",
|
||||
"libsql": "^0.5.22",
|
||||
"libsql-stateless-easy": "^1.8.0",
|
||||
"open": "^10.1.0",
|
||||
"miniflare": "^4.20251011.2",
|
||||
"open": "^10.2.0",
|
||||
"openapi-types": "^12.1.3",
|
||||
"postcss": "^8.5.3",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"postcss-preset-mantine": "^1.18.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"posthog-js-lite": "^3.4.2",
|
||||
"posthog-js-lite": "^3.6.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-icons": "5.2.1",
|
||||
"react-json-view-lite": "^2.4.1",
|
||||
"sql-formatter": "^15.4.11",
|
||||
"react-hook-form": "^7.66.0",
|
||||
"react-icons": "5.5.0",
|
||||
"react-json-view-lite": "^2.5.0",
|
||||
"sql-formatter": "^15.6.10",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss": "^4.0.12",
|
||||
"tailwindcss": "^4.1.16",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tsc-alias": "^1.8.11",
|
||||
"tsup": "^8.4.0",
|
||||
"tsx": "^4.19.3",
|
||||
"uuid": "^11.1.0",
|
||||
"vite": "^6.3.5",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"tsup": "^8.5.0",
|
||||
"tsx": "^4.20.6",
|
||||
"uuid": "^13.0.0",
|
||||
"vite": "^7.1.12",
|
||||
"vite-plugin-circular-dependency": "^0.5.0",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.0.9",
|
||||
"wouter": "^3.6.0",
|
||||
"wrangler": "^4.37.1",
|
||||
"miniflare": "^4.20250913.0"
|
||||
"vitest": "3.0.9",
|
||||
"wouter": "^3.7.1",
|
||||
"wrangler": "^4.45.4"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@hono/node-server": "^1.14.3"
|
||||
"@hono/node-server": "^1.19.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=19",
|
||||
@@ -209,6 +214,11 @@
|
||||
"import": "./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": {
|
||||
"types": "./dist/types/adapter/index.d.ts",
|
||||
"import": "./dist/adapter/index.js"
|
||||
|
||||
+3
-2
@@ -5,7 +5,6 @@ import type { em as prototypeEm } from "data/prototype";
|
||||
import { Connection } from "data/connection/Connection";
|
||||
import type { Hono } from "hono";
|
||||
import {
|
||||
type InitialModuleConfigs,
|
||||
type ModuleConfigs,
|
||||
type Modules,
|
||||
ModuleManager,
|
||||
@@ -381,8 +380,10 @@ export class App<
|
||||
if (results.length > 0) {
|
||||
for (const { name, result } of results) {
|
||||
if (result) {
|
||||
$console.log(`[Plugin:${name}] schema`);
|
||||
ctx.helper.ensureSchema(result);
|
||||
if (ctx.flags.sync_required) {
|
||||
$console.log(`[Plugin:${name}] schema, sync required`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import path from "node:path";
|
||||
import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter";
|
||||
import { registerLocalMediaAdapter } from ".";
|
||||
import { config, type App } from "bknd";
|
||||
import type { ServeOptions } from "bun";
|
||||
import { serveStatic } from "hono/bun";
|
||||
|
||||
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>(
|
||||
{ distPath, serveStatic: _serveStatic, ...config }: BunBkndConfig<Env> = {},
|
||||
@@ -60,7 +58,7 @@ export function serve<Env = BunEnv>(
|
||||
args: Env = Bun.env as Env,
|
||||
) {
|
||||
Bun.serve({
|
||||
...serveOptions,
|
||||
...(serveOptions as any),
|
||||
port,
|
||||
fetch: createHandler(
|
||||
{
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { inspect } from "node:util";
|
||||
|
||||
export type BindingTypeMap = {
|
||||
D1Database: D1Database;
|
||||
KVNamespace: KVNamespace;
|
||||
@@ -10,21 +8,40 @@ export type BindingTypeMap = {
|
||||
export type GetBindingType = keyof BindingTypeMap;
|
||||
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>[] {
|
||||
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) {
|
||||
try {
|
||||
if (
|
||||
(env[key] as any).constructor.name === type ||
|
||||
String(env[key]) === `[object ${type}]` ||
|
||||
inspect(env[key]).includes(type)
|
||||
) {
|
||||
bindings.push({
|
||||
key,
|
||||
value: env[key] as BindingTypeMap[T],
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
const value = env[key];
|
||||
if (typeof value !== "object" || value === null) continue;
|
||||
if (!matcher(value)) continue;
|
||||
|
||||
bindings.push({
|
||||
key,
|
||||
value,
|
||||
} as any);
|
||||
}
|
||||
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"]);
|
||||
});
|
||||
});
|
||||
@@ -16,10 +16,14 @@ export {
|
||||
type GetBindingType,
|
||||
type BindingMap,
|
||||
} from "./bindings";
|
||||
export { constants, makeConfig, type CloudflareContext } from "./config";
|
||||
export {
|
||||
constants,
|
||||
makeConfig,
|
||||
type CloudflareContext,
|
||||
registerAsyncsExecutionContext,
|
||||
} from "./config";
|
||||
export { StorageR2Adapter, registerMedia } from "./storage/StorageR2Adapter";
|
||||
export { registries } from "bknd";
|
||||
export { devFsVitePlugin, devFsWrite } from "./vite";
|
||||
|
||||
// for compatibility with old code
|
||||
export function d1<DB extends D1Database | D1DatabaseSession = D1Database>(
|
||||
|
||||
@@ -16,6 +16,7 @@ export function registerMedia(
|
||||
env: Record<string, any>,
|
||||
registries: typeof $registries = $registries,
|
||||
) {
|
||||
if (registries.media.has("r2")) return;
|
||||
const r2_bindings = getBindings(env, "R2Bucket");
|
||||
|
||||
registries.media.register(
|
||||
@@ -172,7 +173,7 @@ export class StorageR2Adapter extends StorageAdapter {
|
||||
await this.bucket.delete(this.getKey(key));
|
||||
}
|
||||
|
||||
getObjectUrl(key: string): string {
|
||||
getObjectUrl(): string {
|
||||
throw new Error("Method getObjectUrl not implemented.");
|
||||
}
|
||||
|
||||
@@ -183,7 +184,7 @@ export class StorageR2Adapter extends StorageAdapter {
|
||||
return key;
|
||||
}
|
||||
|
||||
toJSON(secrets?: boolean) {
|
||||
toJSON() {
|
||||
return {
|
||||
type: this.getName(),
|
||||
config: {},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// biome-ignore-all lint: .
|
||||
import type { Plugin } from "vite";
|
||||
import { writeFile as nodeWriteFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
@@ -48,7 +49,6 @@ export function devFsVitePlugin({
|
||||
// Skip our own debug output
|
||||
if (output.includes("[dev-fs-plugin]") || output.includes("[dev-fs-polyfill]")) {
|
||||
// @ts-ignore
|
||||
// biome-ignore lint/style/noArguments: <explanation>
|
||||
return originalStdoutWrite.apply(process.stdout, arguments);
|
||||
}
|
||||
|
||||
@@ -178,7 +178,6 @@ export function devFsVitePlugin({
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
// biome-ignore lint:
|
||||
return originalStdoutWrite.apply(process.stdout, arguments);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createFrameworkApp, type FrameworkBkndConfig } from "bknd/adapter";
|
||||
import { isNode } from "bknd/utils";
|
||||
// @ts-expect-error next is not installed
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
type NextjsEnv = NextApiRequest["env"];
|
||||
@@ -18,7 +19,9 @@ function getCleanRequest(req: Request, cleanRequest: NextjsBkndConfig["cleanRequ
|
||||
if (!cleanRequest) return req;
|
||||
|
||||
const url = new URL(req.url);
|
||||
cleanRequest?.searchParams?.forEach((k) => url.searchParams.delete(k));
|
||||
cleanRequest?.searchParams?.forEach((k) => {
|
||||
url.searchParams.delete(k);
|
||||
});
|
||||
|
||||
if (isNode()) {
|
||||
return new Request(url.toString(), {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { resendEmail } from "./resend";
|
||||
const ALL_TESTS = !!process.env.ALL_TESTS;
|
||||
|
||||
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);
|
||||
expect(driver.send("foo@bar.com", "Test", "Test")).rejects.toThrow();
|
||||
});
|
||||
|
||||
+2
-1
@@ -73,8 +73,9 @@ export const env = <
|
||||
onValid?: (valid: R) => void;
|
||||
},
|
||||
): R extends undefined ? Fallback : R => {
|
||||
const source = opts?.source ?? (typeof process !== "undefined" ? process?.env : {});
|
||||
|
||||
try {
|
||||
const source = opts?.source ?? process.env;
|
||||
const c = envs[key];
|
||||
const g = source[c.key];
|
||||
const v = c.validate(g) as any;
|
||||
|
||||
@@ -120,17 +120,14 @@ export function patternMatch(target: string, pattern: RegExp | string): boolean
|
||||
}
|
||||
|
||||
export function slugify(str: string): string {
|
||||
return (
|
||||
String(str)
|
||||
.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.
|
||||
.trim() // trim leading or trailing whitespace
|
||||
.toLowerCase() // convert to lowercase
|
||||
.replace(/[^a-z0-9 -]/g, "") // remove non-alphanumeric characters
|
||||
.replace(/\s+/g, "-") // replace spaces with hyphens
|
||||
.replace(/-+/g, "-") // remove consecutive hyphens
|
||||
);
|
||||
return String(str)
|
||||
.normalize("NFKD") // split accented characters into their base characters and diacritical marks
|
||||
.replace(/[\u0300-\u036f]/g, "") // remove all the accents, which happen to be all in the \u03xx UNICODE block.
|
||||
.trim() // trim leading or trailing whitespace
|
||||
.toLowerCase() // convert to lowercase
|
||||
.replace(/[^a-z0-9 -]/g, "") // remove non-alphanumeric characters
|
||||
.replace(/\s+/g, "-") // replace spaces with hyphens
|
||||
.replace(/-+/g, "-"); // remove consecutive hyphens
|
||||
}
|
||||
|
||||
export function truncate(str: string, length = 50, end = "..."): string {
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
type Simplify,
|
||||
sql,
|
||||
} from "kysely";
|
||||
import type { jsonArrayFrom, jsonBuildObject, jsonObjectFrom } from "kysely/helpers/sqlite";
|
||||
import type { BaseIntrospector, BaseIntrospectorConfig } from "./BaseIntrospector";
|
||||
import type { DB } from "bknd";
|
||||
import type { Constructor } from "core/registry/Registry";
|
||||
@@ -70,15 +71,9 @@ export type IndexSpec = {
|
||||
};
|
||||
|
||||
export type DbFunctions = {
|
||||
jsonObjectFrom<O>(expr: SelectQueryBuilderExpression<O>): RawBuilder<Simplify<O> | null>;
|
||||
jsonArrayFrom<O>(expr: SelectQueryBuilderExpression<O>): RawBuilder<Simplify<O>[]>;
|
||||
jsonBuildObject<O extends Record<string, Expression<unknown>>>(
|
||||
obj: O,
|
||||
): RawBuilder<
|
||||
Simplify<{
|
||||
[K in keyof O]: O[K] extends Expression<infer V> ? V : never;
|
||||
}>
|
||||
>;
|
||||
jsonObjectFrom: typeof jsonObjectFrom;
|
||||
jsonArrayFrom: typeof jsonArrayFrom;
|
||||
jsonBuildObject: typeof jsonBuildObject;
|
||||
};
|
||||
|
||||
export type ConnQuery = CompiledQuery | Compilable;
|
||||
|
||||
@@ -18,7 +18,7 @@ export type LibsqlClientFns = {
|
||||
function getClient(clientOrCredentials: Client | LibSqlCredentials | LibsqlClientFns): Client {
|
||||
if (clientOrCredentials && "url" in clientOrCredentials) {
|
||||
const { url, authToken } = clientOrCredentials;
|
||||
return createClient({ url, authToken });
|
||||
return createClient({ url, authToken }) as unknown as Client;
|
||||
}
|
||||
|
||||
return clientOrCredentials as Client;
|
||||
|
||||
@@ -136,10 +136,8 @@ export class Entity<
|
||||
.map((field) => (alias ? `${alias}.${field.name} as ${field.name}` : field.name));
|
||||
}
|
||||
|
||||
getFillableFields(context?: "create" | "update", include_virtual?: boolean): Field[] {
|
||||
return this.getFields({ virtual: include_virtual }).filter((field) =>
|
||||
field.isFillable(context),
|
||||
);
|
||||
getFillableFields(context?: TActionContext, include_virtual?: boolean): Field[] {
|
||||
return this.getFields(include_virtual).filter((field) => field.isFillable(context));
|
||||
}
|
||||
|
||||
getRequiredFields(): Field[] {
|
||||
@@ -191,15 +189,9 @@ export class Entity<
|
||||
return this.fields.findIndex((field) => field.name === name) !== -1;
|
||||
}
|
||||
|
||||
getFields({
|
||||
virtual = false,
|
||||
primary = true,
|
||||
}: { 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;
|
||||
});
|
||||
getFields(include_virtual: boolean = false): Field[] {
|
||||
if (include_virtual) return this.fields;
|
||||
return this.fields.filter((f) => !f.isVirtual());
|
||||
}
|
||||
|
||||
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) {
|
||||
const field_names = fields.map((f) => f.name);
|
||||
@@ -283,7 +275,7 @@ export class Entity<
|
||||
fields = this.getFillableFields(options.context);
|
||||
break;
|
||||
default:
|
||||
fields = this.getFields({ virtual: true });
|
||||
fields = this.getFields(true);
|
||||
}
|
||||
|
||||
const _fields = Object.fromEntries(fields.map((field) => [field.name, field]));
|
||||
|
||||
@@ -83,10 +83,8 @@ export class Mutator<
|
||||
}
|
||||
|
||||
// we should never get here, but just to be sure (why?)
|
||||
if (!field.isFillable(context as any)) {
|
||||
throw new Error(
|
||||
`Field "${key}" of entity "${entity.name}" is not fillable on context "${context}"`,
|
||||
);
|
||||
if (!field.isFillable(context)) {
|
||||
throw new Error(`Field "${key}" is not fillable on entity "${entity.name}"`);
|
||||
}
|
||||
|
||||
// transform from field
|
||||
|
||||
@@ -26,19 +26,11 @@ export const baseFieldConfigSchema = s
|
||||
.strictObject({
|
||||
label: s.string(),
|
||||
description: s.string(),
|
||||
required: s.boolean({ default: DEFAULT_REQUIRED }),
|
||||
fillable: s.anyOf(
|
||||
[
|
||||
s.boolean({ title: "Boolean" }),
|
||||
s.array(s.string({ enum: ["create", "update"] }), {
|
||||
title: "Context",
|
||||
uniqueItems: true,
|
||||
}),
|
||||
],
|
||||
{
|
||||
default: DEFAULT_FILLABLE,
|
||||
},
|
||||
),
|
||||
required: s.boolean({ default: false }),
|
||||
fillable: s.anyOf([
|
||||
s.boolean({ title: "Boolean" }),
|
||||
s.array(s.string({ enum: ActionContext }), { title: "Context", uniqueItems: true }),
|
||||
]),
|
||||
hidden: s.anyOf([
|
||||
s.boolean({ title: "Boolean" }),
|
||||
// @todo: tmp workaround
|
||||
@@ -111,7 +103,7 @@ export abstract class Field<
|
||||
return this.config?.default_value;
|
||||
}
|
||||
|
||||
isFillable(context?: "create" | "update"): boolean {
|
||||
isFillable(context?: TActionContext): boolean {
|
||||
if (Array.isArray(this.config.fillable)) {
|
||||
return context ? this.config.fillable.includes(context) : DEFAULT_FILLABLE;
|
||||
}
|
||||
@@ -173,7 +165,7 @@ export abstract class Field<
|
||||
// @todo: add field level validation
|
||||
isValid(value: any, context: TActionContext): boolean {
|
||||
if (typeof value !== "undefined") {
|
||||
return this.isFillable(context as any);
|
||||
return this.isFillable(context);
|
||||
} else if (context === "create") {
|
||||
return !this.isRequired();
|
||||
}
|
||||
|
||||
@@ -99,7 +99,6 @@ export function fieldTestSuite(
|
||||
const _config = {
|
||||
..._requiredConfig,
|
||||
required: false,
|
||||
fillable: true,
|
||||
};
|
||||
|
||||
function fieldJson(field: Field) {
|
||||
@@ -117,7 +116,10 @@ export function fieldTestSuite(
|
||||
|
||||
expect(fieldJson(fillable)).toEqual({
|
||||
type: noConfigField.type,
|
||||
config: _config,
|
||||
config: {
|
||||
..._config,
|
||||
fillable: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(fieldJson(required)).toEqual({
|
||||
@@ -148,6 +150,7 @@ export function fieldTestSuite(
|
||||
type: requiredAndDefault.type,
|
||||
config: {
|
||||
..._config,
|
||||
fillable: true,
|
||||
required: true,
|
||||
default_value: config.defaultValue,
|
||||
},
|
||||
|
||||
@@ -77,7 +77,7 @@ export class SchemaManager {
|
||||
}
|
||||
|
||||
getIntrospectionFromEntity(entity: Entity): IntrospectedTable {
|
||||
const fields = entity.getFields({ virtual: false });
|
||||
const fields = entity.getFields(false);
|
||||
const indices = this.em.getIndicesOf(entity);
|
||||
|
||||
// this is intentionally setting values to defaults, like "nullable" and "default"
|
||||
|
||||
+11
-13
@@ -23,26 +23,17 @@ export type HybridMode<AdapterConfig extends BkndConfig> = AdapterConfig extends
|
||||
? BkndModeConfig<Args, Merge<BkndHybridModeOptions & AdapterConfig>>
|
||||
: never;
|
||||
|
||||
export function hybrid<Args>({
|
||||
configFilePath = "bknd-config.json",
|
||||
...rest
|
||||
}: HybridBkndConfig<Args>): BkndConfig<Args> {
|
||||
export function hybrid<Args>(hybridConfig: HybridBkndConfig<Args>): BkndConfig<Args> {
|
||||
return {
|
||||
...rest,
|
||||
config: undefined,
|
||||
app: async (args) => {
|
||||
const {
|
||||
config: appConfig,
|
||||
isProd,
|
||||
plugins,
|
||||
syncSchemaOptions,
|
||||
} = await makeModeConfig(
|
||||
{
|
||||
...rest,
|
||||
configFilePath,
|
||||
},
|
||||
args,
|
||||
);
|
||||
} = await makeModeConfig(hybridConfig, args);
|
||||
|
||||
const configFilePath = appConfig.configFilePath ?? "bknd-config.json";
|
||||
|
||||
if (appConfig?.options?.mode && appConfig?.options?.mode !== "db") {
|
||||
$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,
|
||||
// secrets are required for hybrid mode
|
||||
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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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 * as $diff from "core/object/diff";
|
||||
import type { Connection } from "data/connection";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { transformObject } from "bknd/utils";
|
||||
import type { Kysely } from "kysely";
|
||||
import { set } from "lodash-es";
|
||||
import type { InitialModuleConfigs } from "modules/ModuleManager";
|
||||
|
||||
export type MigrationContext = {
|
||||
db: Kysely<any>;
|
||||
@@ -108,29 +107,6 @@ export const migrations: Migration[] = [
|
||||
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;
|
||||
|
||||
@@ -30,7 +30,7 @@ export const Group = <E extends ElementType = "div">({
|
||||
{...props}
|
||||
data-role="group"
|
||||
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" && error && "border-red-500",
|
||||
error && "text-red-500",
|
||||
@@ -97,7 +97,7 @@ export const Input = forwardRef<HTMLInputElement, React.ComponentProps<"input">>
|
||||
ref={ref}
|
||||
className={twMerge(
|
||||
"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 &&
|
||||
"focus:bg-muted focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all",
|
||||
props.className,
|
||||
@@ -154,7 +154,7 @@ export const Textarea = forwardRef<HTMLTextAreaElement, React.ComponentProps<"te
|
||||
{...props}
|
||||
ref={ref}
|
||||
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,
|
||||
)}
|
||||
/>
|
||||
@@ -214,7 +214,7 @@ export const BooleanInput = forwardRef<HTMLInputElement, React.ComponentProps<"i
|
||||
{...props}
|
||||
type="checkbox"
|
||||
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}
|
||||
onChange={handleCheck}
|
||||
disabled={props.disabled}
|
||||
@@ -260,6 +260,7 @@ export const Switch = forwardRef<
|
||||
props.disabled && "opacity-50 !cursor-not-allowed",
|
||||
)}
|
||||
onCheckedChange={(bool) => {
|
||||
console.log("setting", bool);
|
||||
props.onChange?.({ target: { value: bool } });
|
||||
}}
|
||||
{...(props as any)}
|
||||
@@ -293,7 +294,7 @@ export const Select = forwardRef<
|
||||
{...props}
|
||||
ref={ref}
|
||||
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",
|
||||
!props.multiple && "border-r-8 border-r-transparent",
|
||||
props.className,
|
||||
|
||||
@@ -19,9 +19,10 @@ import type { RelationField } from "data/relations";
|
||||
import { useEntityAdminOptions } from "ui/options";
|
||||
|
||||
// simplify react form types 🤦
|
||||
export type FormApi = ReactFormExtendedApi<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>;
|
||||
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 = {
|
||||
entity: Entity;
|
||||
@@ -44,7 +45,7 @@ export function EntityForm({
|
||||
className,
|
||||
action,
|
||||
}: EntityFormProps) {
|
||||
const fields = entity.getFields({ virtual: true, primary: false });
|
||||
const fields = entity.getFillableFields(action, true);
|
||||
const options = useEntityAdminOptions(entity, action);
|
||||
|
||||
return (
|
||||
@@ -92,6 +93,10 @@ export function EntityForm({
|
||||
);
|
||||
}
|
||||
|
||||
if (!field.isFillable(action)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const _key = `${entity.name}-${field.name}-${key}`;
|
||||
|
||||
return (
|
||||
@@ -123,7 +128,7 @@ export function EntityForm({
|
||||
<EntityFormField
|
||||
field={field}
|
||||
fieldApi={props}
|
||||
disabled={fieldsDisabled || !field.isFillable(action)}
|
||||
disabled={fieldsDisabled}
|
||||
tabIndex={key + 1}
|
||||
action={action}
|
||||
data={data}
|
||||
|
||||
@@ -17,7 +17,7 @@ export function useEntityForm({
|
||||
}: EntityFormProps) {
|
||||
const data = initialData ?? {};
|
||||
// @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
|
||||
const defaultValues = getDefaultValues(fields, data);
|
||||
|
||||
@@ -207,7 +207,7 @@ function DataEntityUpdateImpl({ params }) {
|
||||
handleSubmit={handleSubmit}
|
||||
fieldsDisabled={fieldsDisabled}
|
||||
data={data ?? undefined}
|
||||
Form={Form}
|
||||
Form={Form as any}
|
||||
action="update"
|
||||
className="flex flex-grow flex-col gap-3 p-3"
|
||||
/>
|
||||
|
||||
@@ -121,7 +121,7 @@ export function DataEntityCreate({ params }) {
|
||||
handleSubmit={handleSubmit}
|
||||
fieldsDisabled={fieldsDisabled}
|
||||
data={search.value}
|
||||
Form={Form}
|
||||
Form={Form as any}
|
||||
action="create"
|
||||
className="flex flex-grow flex-col gap-3 p-3"
|
||||
/>
|
||||
|
||||
@@ -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";
|
||||
|
||||
export function extractSchema<
|
||||
@@ -10,7 +10,7 @@ export function extractSchema<
|
||||
config: Config,
|
||||
keys: Keys[],
|
||||
): [
|
||||
JSONSchema7,
|
||||
Exclude<JSONSchema, boolean | null | undefined>,
|
||||
Partial<Config>,
|
||||
{
|
||||
[K in Keys]: {
|
||||
|
||||
@@ -5,7 +5,7 @@ import path from "node:path";
|
||||
export default defineConfig({
|
||||
plugins: [tsconfigPaths()],
|
||||
test: {
|
||||
projects: ["**/*.vitest.config.ts"],
|
||||
projects: ["**/*.vitest.config.ts", "**/*/vitest.config.ts"],
|
||||
include: ["**/*.vi-test.ts", "**/*.vitest.ts"],
|
||||
},
|
||||
});
|
||||
|
||||
+29
-22
@@ -1,14 +1,13 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"$schema": "https://biomejs.dev/schemas/2.3.3/schema.json",
|
||||
"assist": { "actions": { "source": { "organizeImports": "off" } } },
|
||||
"vcs": {
|
||||
"defaultBranch": "main"
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space"
|
||||
"indentStyle": "space",
|
||||
"formatWithErrors": true
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
@@ -30,32 +29,36 @@
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"ignore": [
|
||||
"**/node_modules/**",
|
||||
"node_modules/**",
|
||||
"**/.cache/**",
|
||||
"**/.wrangler/**",
|
||||
"**/build/**",
|
||||
"**/dist/**",
|
||||
"**/data.sqld/**",
|
||||
"data.sqld/**",
|
||||
"public/**",
|
||||
".history/**"
|
||||
"includes": [
|
||||
"**",
|
||||
"!!**/node_modules",
|
||||
"!!**/.cache",
|
||||
"!!**/.wrangler",
|
||||
"!!**/build",
|
||||
"!!**/dist",
|
||||
"!!**/data.sqld",
|
||||
"!!**/data.sqld",
|
||||
"!!**/public",
|
||||
"!!**/.history"
|
||||
]
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"ignore": ["**/*.spec.ts"],
|
||||
"includes": ["**"],
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"a11y": {
|
||||
"all": false
|
||||
},
|
||||
"a11y": {},
|
||||
"correctness": {
|
||||
"useExhaustiveDependencies": "off",
|
||||
"noUnreachable": "warn",
|
||||
"noChildrenProp": "off",
|
||||
"noSwitchDeclarations": "warn"
|
||||
"noSwitchDeclarations": "warn",
|
||||
"noUnusedVariables": {
|
||||
"options": {
|
||||
"ignoreRestSiblings": true
|
||||
},
|
||||
"level": "warn"
|
||||
}
|
||||
},
|
||||
"complexity": {
|
||||
"noUselessFragments": "warn",
|
||||
@@ -70,7 +73,11 @@
|
||||
"noArrayIndexKey": "off",
|
||||
"noImplicitAnyLet": "warn",
|
||||
"noConfusingVoidType": "off",
|
||||
"noConsoleLog": "warn"
|
||||
"noConsole": {
|
||||
"level": "warn",
|
||||
"options": { "allow": ["error", "info"] }
|
||||
},
|
||||
"noTsIgnore": "off"
|
||||
},
|
||||
"security": {
|
||||
"noDangerouslySetInnerHtml": "off"
|
||||
|
||||
@@ -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:
|
||||
|
||||
```ts
|
||||
import { devFsVitePlugin } from "bknd/adapter/cloudflare";
|
||||
import { devFsVitePlugin } from "bknd/adapter/cloudflare/vite";
|
||||
|
||||
export default defineConfig({
|
||||
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.
|
||||
|
||||
```ts
|
||||
import { devFsWrite } from "bknd/adapter/cloudflare"; // [!code highlight]
|
||||
import { devFsWrite } from "bknd/adapter/cloudflare/vite"; // [!code highlight]
|
||||
import { syncTypes } from "bknd/plugins";
|
||||
|
||||
export default {
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
"scripts": {
|
||||
"deploy": "wrangler deploy",
|
||||
"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",
|
||||
"predev": "npm run typegen"
|
||||
"postinstall": "npm run typegen"
|
||||
},
|
||||
"dependencies": {
|
||||
"bknd": "file:../../app"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.2",
|
||||
"wrangler": "^4.34.0"
|
||||
"typescript": "^5.9.3",
|
||||
"wrangler": "^4.46.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
"$schema": "node_modules/wrangler/config-schema.json",
|
||||
"name": "bknd-cf-worker-example",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2025-08-03",
|
||||
"compatibility_flags": ["nodejs_compat"],
|
||||
"compatibility_date": "2025-11-05",
|
||||
"workers_dev": true,
|
||||
"minify": true,
|
||||
"assets": {
|
||||
|
||||
+6
-6
@@ -12,13 +12,13 @@
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@tsconfig/strictest": "^2.0.5",
|
||||
"@biomejs/biome": "2.3.3",
|
||||
"@tsconfig/strictest": "^2.0.7",
|
||||
"@types/bun": "^1.3.1",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"bun-types": "^1.1.18",
|
||||
"miniflare": "^3.20240806.0",
|
||||
"typescript": "^5.5.3",
|
||||
"verdaccio": "^5.32.1"
|
||||
"miniflare": "^3.20250718.2",
|
||||
"typescript": "^5.9.3",
|
||||
"verdaccio": "^6.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
|
||||
Reference in New Issue
Block a user