Compare commits

..

14 Commits

Author SHA1 Message Date
dswbx 7a046fa18e updated references 2025-07-07 08:39:21 +02:00
dswbx 786d1d1bd4 init media adapter refactoring 2025-07-05 18:12:55 +02:00
dswbx c384bf4dd4 remove unneeded vite dep 2025-07-05 14:17:26 +02:00
dswbx db2a994a01 Merge pull request #192 from bknd-io/feat/jsonv-refactor
feat/jsonv-refactor
2025-07-05 14:02:22 +02:00
dswbx 166ea4a71b removed @sinclair/typebox 2025-07-05 13:57:36 +02:00
dswbx 0d3bb3b7d6 added validator for rjsf, hook form via standard schema 2025-07-05 13:29:30 +02:00
dswbx cfbec5b6ea Merge remote-tracking branch 'origin/main' into feat/jsonv-refactor
# Conflicts:
#	bun.lock
2025-07-05 11:11:06 +02:00
dswbx 5143ee5726 updated schemas, fixed tests, skipping flow tests for now 2025-07-03 14:00:29 +02:00
dswbx fe1716ed01 fix secret schema 2025-07-02 17:59:38 +02:00
dswbx 0c31dcdb95 updated lock 2025-07-02 17:58:47 +02:00
dswbx 4cc0f8e172 Merge branch 'release/0.15' into feat/jsonv-refactor
# Conflicts:
#	app/build.ts
#	app/package.json
#	app/src/App.ts
#	app/src/adapter/cloudflare/storage/StorageR2Adapter.ts
#	app/src/auth/authenticate/Authenticator.ts
#	app/src/auth/authenticate/strategies/PasswordStrategy.ts
#	app/src/data/entities/Entity.ts
#	app/src/data/fields/DateField.ts
#	app/src/data/server/query.ts
#	app/src/flows/flows/triggers/EventTrigger.ts
#	app/src/flows/tasks/presets/LogTask.ts
#	app/src/media/AppMedia.ts
#	app/src/modules/server/AppServer.ts
#	app/src/modules/server/SystemController.ts
#	app/vite.dev.ts
#	bun.lock
2025-07-02 17:18:12 +02:00
dswbx c161a26ec0 test secrets extraction 2025-06-25 07:49:39 +02:00
dswbx 6e78a4c238 fixes 2025-06-21 17:05:27 +02:00
dswbx 42edce904f initial refactor 2025-06-21 13:35:58 +02:00
611 changed files with 12662 additions and 29086 deletions
-7
View File
@@ -1,7 +0,0 @@
{
"mcpServers": {
"bknd": {
"url": "http://localhost:28623/api/system/mcp"
}
}
}
+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.2.14"
- name: Install dependencies - name: Install dependencies
working-directory: ./app working-directory: ./app
+1 -3
View File
@@ -31,6 +31,4 @@ packages/media/.env
.git_old .git_old
docker/tmp docker/tmp
.debug .debug
.history .history
.aider*
.vercel
+5 -6
View File
@@ -1,9 +1,9 @@
[![npm version](https://img.shields.io/npm/v/bknd.svg)](https://npmjs.org/package/bknd) [![npm version](https://img.shields.io/npm/v/bknd.svg)](https://npmjs.org/package/bknd)
![bknd](https://raw.githubusercontent.com/bknd-io/bknd/refs/heads/main/docs/public/assets/poster.png) ![bknd](https://raw.githubusercontent.com/bknd-io/bknd/refs/heads/main/docs/_assets/poster.png)
<p align="center" width="100%"> <p align="center" width="100%">
<a href="https://stackblitz.com/github/bknd-io/bknd-demo?hideExplorer=1&embed=1&view=preview&initialPath=%2Fdata%2Fschema" target="_blank"> <a href="https://stackblitz.com/github/bknd-io/bknd-examples?hideExplorer=1&embed=1&view=preview&startScript=example-admin-rich&initialPath=%2Fdata%2Fschema" target="_blank">
<strong>⭐ Live Demo</strong> <strong>⭐ Live Demo</strong>
</a> </a>
</p> </p>
@@ -19,7 +19,7 @@ bknd simplifies app development by providing a fully functional backend for data
**For documentation and examples, please visit https://docs.bknd.io.** **For documentation and examples, please visit https://docs.bknd.io.**
> [!WARNING] > [!WARNING]
> This project requires Node.js 22.13 or higher (because of `node:sqlite`). > This project requires Node.js 22 or higher (because of `node:sqlite`).
> >
> Please keep in mind that **bknd** is still under active development > Please keep in mind that **bknd** is still under active development
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0. > and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
@@ -27,8 +27,8 @@ bknd simplifies app development by providing a fully functional backend for data
## Size ## Size
![gzipped size of bknd](https://img.shields.io/bundlejs/size/bknd?label=bknd) ![gzipped size of bknd](https://img.shields.io/bundlejs/size/bknd?label=bknd)
![gzipped size of bknd/client](https://img.badgesize.io/https://unpkg.com/bknd@latest/dist/ui/client/index.js?compression=gzip&label=bknd/client) ![gzipped size of bknd/client](https://img.badgesize.io/https://unpkg.com/bknd@latest/dist/ui/client/index.js?compression=gzip&label=bknd/client)
![gzipped size of bknd/elements](https://img.badgesize.io/https://unpkg.com/bknd@latest/dist/ui/elements/index.js?compression=gzip&label=bknd/elements&t=1) ![gzipped size of bknd/elements](https://img.badgesize.io/https://unpkg.com/bknd@latest/dist/ui/elements/index.js?compression=gzip&label=bknd/elements)
![gzipped size of bknd/ui](https://img.badgesize.io/https://unpkg.com/bknd@latest/dist/ui/index.js?compression=gzip&label=bknd/ui&t=1) ![gzipped size of bknd/ui](https://img.badgesize.io/https://unpkg.com/bknd@latest/dist/ui/index.js?compression=gzip&label=bknd/ui)
The size on npm is misleading, as the `bknd` package includes the backend, the ui components as well as the whole backend bundled into the cli including static assets. The size on npm is misleading, as the `bknd` package includes the backend, the ui components as well as the whole backend bundled into the cli including static assets.
@@ -52,7 +52,6 @@ Creating digital products always requires developing both the backend (the logic
* using a React framework (Next.js, React Router, Astro) * using a React framework (Next.js, React Router, Astro)
* 📦 Official API and React SDK with type-safety * 📦 Official API and React SDK with type-safety
* ⚛️ React elements for auto-configured authentication and media components * ⚛️ React elements for auto-configured authentication and media components
* 🛠️ MCP server, client and UI built-in to control your backend
## Structure ## Structure
The package is mainly split into 4 parts, each serving a specific purpose: The package is mainly split into 4 parts, each serving a specific purpose:
+3 -6
View File
@@ -1,15 +1,12 @@
import { afterEach, describe, test, expect, beforeAll, afterAll } from "bun:test"; import { afterEach, describe, test, expect } from "bun:test";
import { App, createApp } from "core/test/utils"; import { App, createApp } from "core/test/utils";
import { getDummyConnection } from "./helper"; import { getDummyConnection } from "./helper";
import { Hono } from "hono"; import { Hono } from "hono";
import * as proto from "../src/data/prototype"; import * as proto from "../src/data/prototype";
import { pick } from "lodash-es"; import { pick } from "lodash-es";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
beforeAll(() => disableConsoleLog());
const { dummyConnection, afterAllCleanup } = getDummyConnection(); const { dummyConnection, afterAllCleanup } = getDummyConnection();
afterEach(async () => (await afterAllCleanup()) && enableConsoleLog()); afterEach(afterAllCleanup);
describe("App tests", async () => { describe("App tests", async () => {
test("boots and pongs", async () => { test("boots and pongs", async () => {
@@ -22,7 +19,7 @@ describe("App tests", async () => {
test("plugins", async () => { test("plugins", async () => {
const called: string[] = []; const called: string[] = [];
const app = createApp({ const app = createApp({
config: { initialConfig: {
auth: { auth: {
enabled: true, enabled: true,
}, },
+1 -2
View File
@@ -1,2 +1 @@
tmp/* tmp/*
!tmp/.gitkeep
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
hello
+40 -29
View File
@@ -1,4 +1,4 @@
import { expect, describe, it, beforeAll, afterAll, mock } from "bun:test"; import { expect, describe, it, beforeAll, afterAll } from "bun:test";
import * as adapter from "adapter"; import * as adapter from "adapter";
import { disableConsoleLog, enableConsoleLog } from "core/utils"; import { disableConsoleLog, enableConsoleLog } from "core/utils";
import { adapterTestSuite } from "adapter/adapter-test-suite"; import { adapterTestSuite } from "adapter/adapter-test-suite";
@@ -9,49 +9,60 @@ beforeAll(disableConsoleLog);
afterAll(enableConsoleLog); afterAll(enableConsoleLog);
describe("adapter", () => { describe("adapter", () => {
it("makes config", async () => { it("makes config", () => {
expect(omitKeys(await adapter.makeConfig({}), ["connection"])).toEqual({}); expect(omitKeys(adapter.makeConfig({}), ["connection"])).toEqual({});
expect( expect(omitKeys(adapter.makeConfig({}, { env: { TEST: "test" } }), ["connection"])).toEqual(
omitKeys(await adapter.makeConfig({}, { env: { TEST: "test" } }), ["connection"]), {},
).toEqual({}); );
// merges everything returned from `app` with the config // merges everything returned from `app` with the config
expect( expect(
omitKeys( omitKeys(
await adapter.makeConfig( adapter.makeConfig(
{ app: (a) => ({ config: { server: { cors: { origin: a.env.TEST } } } }) }, { app: (a) => ({ initialConfig: { server: { cors: { origin: a.env.TEST } } } }) },
{ env: { TEST: "test" } }, { env: { TEST: "test" } },
), ),
["connection"], ["connection"],
), ),
).toEqual({ ).toEqual({
config: { server: { cors: { origin: "test" } } }, initialConfig: { server: { cors: { origin: "test" } } },
}); });
}); });
it("allows all properties in app function", async () => { /* it.only("...", async () => {
const called = mock(() => null); const app = await adapter.createAdapterApp();
const config = await adapter.makeConfig( }); */
it("reuses apps correctly", async () => {
const id = crypto.randomUUID();
const first = await adapter.createAdapterApp(
{ {
app: (env) => ({ initialConfig: { server: { cors: { origin: "random" } } },
connection: { url: "test" },
config: { server: { cors: { origin: "test" } } },
options: {
mode: "db",
},
onBuilt: () => {
called();
expect(env).toEqual({ foo: "bar" });
},
}),
}, },
{ foo: "bar" }, undefined,
{ id },
); );
expect(config.connection).toEqual({ url: "test" }); const second = await adapter.createAdapterApp();
expect(config.config).toEqual({ server: { cors: { origin: "test" } } }); const third = await adapter.createAdapterApp(undefined, undefined, { id });
expect(config.options).toEqual({ mode: "db" });
await config.onBuilt?.(null as any); await first.build();
expect(called).toHaveBeenCalled(); await second.build();
await third.build();
expect(first.toJSON().server.cors.origin).toEqual("random");
expect(first).toBe(third);
expect(first).not.toBe(second);
expect(second).not.toBe(third);
expect(second.toJSON().server.cors.origin).toEqual("*");
// recreate the first one
const first2 = await adapter.createAdapterApp(undefined, undefined, { id, force: true });
await first2.build();
expect(first2).not.toBe(first);
expect(first2).not.toBe(third);
expect(first2).not.toBe(second);
expect(first2.toJSON().server.cors.origin).toEqual("*");
}); });
adapterTestSuite(bunTestRunner, { adapterTestSuite(bunTestRunner, {
+1
View File
@@ -42,6 +42,7 @@ describe("Api", async () => {
expect(api.isAuthVerified()).toBe(false); expect(api.isAuthVerified()).toBe(false);
const params = api.getParams(); const params = api.getParams();
console.log(params);
expect(params.token).toBe(token); expect(params.token).toBe(token);
expect(params.token_transport).toBe("cookie"); expect(params.token_transport).toBe("cookie");
expect(params.host).toBe("http://example.com"); expect(params.host).toBe("http://example.com");
+3 -3
View File
@@ -1,12 +1,12 @@
import { afterAll, beforeAll, describe, expect, it } from "bun:test"; import { afterAll, beforeAll, describe, expect, it } from "bun:test";
import { Guard } from "../../src/auth/authorize/Guard"; import { Guard } from "../../src/auth";
import { DataApi } from "../../src/data/api/DataApi"; import { DataApi } from "../../src/data/api/DataApi";
import { DataController } from "../../src/data/api/DataController"; import { DataController } from "../../src/data/api/DataController";
import { dataConfigSchema } from "../../src/data/data-schema"; import { dataConfigSchema } from "../../src/data/data-schema";
import * as proto from "../../src/data/prototype"; import * as proto from "../../src/data/prototype";
import { schemaToEm } from "../helper"; import { schemaToEm } from "../helper";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test"; import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
import { parse } from "core/utils/schema"; import { parse } from "core/object/schema";
beforeAll(disableConsoleLog); beforeAll(disableConsoleLog);
afterAll(enableConsoleLog); afterAll(enableConsoleLog);
@@ -202,7 +202,7 @@ describe("DataApi", () => {
{ {
// create many // create many
const res = await api.createMany("posts", payload); const res = await api.createMany("posts", payload);
expect(res.data?.length).toEqual(4); expect(res.data.length).toEqual(4);
expect(res.ok).toBeTrue(); expect(res.ok).toBeTrue();
} }
+3 -35
View File
@@ -1,23 +1,9 @@
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test"; import { describe, expect, mock, test } from "bun:test";
import type { ModuleBuildContext } from "../../src"; import type { ModuleBuildContext } from "../../src";
import { App, createApp } from "core/test/utils"; import { App, createApp } from "core/test/utils";
import * as proto from "data/prototype"; import * as proto from "../../src/data/prototype";
import { DbModuleManager } from "modules/db/DbModuleManager";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
describe("App", () => { describe("App", () => {
test("use db mode by default", async () => {
const app = createApp();
await app.build();
expect(app.mode).toBe("db");
expect(app.isReadOnly()).toBe(false);
expect(app.modules instanceof DbModuleManager).toBe(true);
});
test("seed includes ctx and app", async () => { test("seed includes ctx and app", async () => {
const called = mock(() => null); const called = mock(() => null);
await createApp({ await createApp({
@@ -34,7 +20,6 @@ describe("App", () => {
"guard", "guard",
"flags", "flags",
"logger", "logger",
"mcp",
"helper", "helper",
]); ]);
}, },
@@ -43,7 +28,7 @@ describe("App", () => {
expect(called).toHaveBeenCalled(); expect(called).toHaveBeenCalled();
const app = createApp({ const app = createApp({
config: { initialConfig: {
data: proto data: proto
.em({ .em({
todos: proto.entity("todos", { todos: proto.entity("todos", {
@@ -150,21 +135,4 @@ describe("App", () => {
// expect async listeners to be executed sync after request // expect async listeners to be executed sync after request
expect(called).toHaveBeenCalled(); expect(called).toHaveBeenCalled();
}); });
test("getMcpClient", async () => {
const app = createApp({
config: {
server: {
mcp: {
enabled: true,
},
},
},
});
await app.build();
const client = app.getMcpClient();
const res = await client.listTools();
expect(res).toBeDefined();
expect(res?.tools.length).toBeGreaterThan(0);
});
}); });
-12
View File
@@ -8,16 +8,10 @@ describe("AppServer", () => {
expect(server).toBeDefined(); expect(server).toBeDefined();
expect(server.config).toEqual({ expect(server.config).toEqual({
cors: { cors: {
allow_credentials: true,
origin: "*", origin: "*",
allow_methods: ["GET", "POST", "PATCH", "PUT", "DELETE"], allow_methods: ["GET", "POST", "PATCH", "PUT", "DELETE"],
allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"], allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"],
}, },
mcp: {
enabled: false,
path: "/api/system/mcp",
logLevel: "warning",
},
}); });
} }
@@ -31,16 +25,10 @@ describe("AppServer", () => {
expect(server).toBeDefined(); expect(server).toBeDefined();
expect(server.config).toEqual({ expect(server.config).toEqual({
cors: { cors: {
allow_credentials: true,
origin: "https", origin: "https",
allow_methods: ["GET", "POST"], allow_methods: ["GET", "POST"],
allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"], allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"],
}, },
mcp: {
enabled: false,
path: "/api/system/mcp",
logLevel: "warning",
},
}); });
} }
}); });
-127
View File
@@ -1,127 +0,0 @@
import { describe, expect, mock, test } from "bun:test";
import { createApp as internalCreateApp, type CreateAppConfig } from "bknd";
import { getDummyConnection } from "../../__test__/helper";
import { ModuleManager } from "modules/ModuleManager";
import { em, entity, text } from "data/prototype";
async function createApp(config: CreateAppConfig = {}) {
const app = internalCreateApp({
connection: getDummyConnection().dummyConnection,
...config,
options: {
...config.options,
mode: "code",
},
});
await app.build();
return app;
}
describe("code-only", () => {
test("should create app with correct manager", async () => {
const app = await createApp();
await app.build();
expect(app.version()).toBeDefined();
expect(app.modules).toBeInstanceOf(ModuleManager);
});
test("should not perform database syncs", async () => {
const app = await createApp({
config: {
data: em({
test: entity("test", {
name: text(),
}),
}).toJSON(),
},
});
expect(app.em.entities.map((e) => e.name)).toEqual(["test"]);
expect(
await app.em.connection.kysely
.selectFrom("sqlite_master")
.where("type", "=", "table")
.selectAll()
.execute(),
).toEqual([]);
// only perform when explicitly forced
await app.em.schema().sync({ force: true });
expect(
await app.em.connection.kysely
.selectFrom("sqlite_master")
.where("type", "=", "table")
.selectAll()
.execute()
.then((r) => r.map((r) => r.name)),
).toEqual(["test", "sqlite_sequence"]);
});
test("should not perform seeding", async () => {
const called = mock(() => null);
const app = await createApp({
config: {
data: em({
test: entity("test", {
name: text(),
}),
}).toJSON(),
},
options: {
seed: async (ctx) => {
called();
await ctx.em.mutator("test").insertOne({ name: "test" });
},
},
});
await app.em.schema().sync({ force: true });
expect(called).not.toHaveBeenCalled();
expect(
await app.em
.repo("test")
.findMany({})
.then((r) => r.data),
).toEqual([]);
});
test("should sync and perform seeding", async () => {
const called = mock(() => null);
const app = await createApp({
config: {
data: em({
test: entity("test", {
name: text(),
}),
}).toJSON(),
},
options: {
seed: async (ctx) => {
called();
await ctx.em.mutator("test").insertOne({ name: "test" });
},
},
});
await app.em.schema().sync({ force: true });
await app.options?.seed?.({
...app.modules.ctx(),
app: app,
});
expect(called).toHaveBeenCalled();
expect(
await app.em
.repo("test")
.findMany({})
.then((r) => r.data),
).toEqual([{ id: 1, name: "test" }]);
});
test("should not allow to modify config", async () => {
const app = await createApp();
// biome-ignore lint/suspicious/noPrototypeBuiltins: <explanation>
expect(app.modules.hasOwnProperty("mutateConfigSafe")).toBe(false);
expect(() => {
app.modules.configs().auth.enabled = true;
}).toThrow();
});
});
-227
View File
@@ -1,227 +0,0 @@
import { describe, test, expect, beforeEach, beforeAll, afterAll } from "bun:test";
import { type App, createApp, createMcpToolCaller } from "core/test/utils";
import { disableConsoleLog, enableConsoleLog } from "core/utils";
import type { McpServer } from "bknd/utils";
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
/**
* - [x] auth_me
* - [x] auth_strategies
* - [x] auth_user_create
* - [x] auth_user_token
* - [x] auth_user_password_change
* - [x] auth_user_password_test
* - [x] config_auth_get
* - [x] config_auth_update
* - [x] config_auth_strategies_get
* - [x] config_auth_strategies_add
* - [x] config_auth_strategies_update
* - [x] config_auth_strategies_remove
* - [x] config_auth_roles_get
* - [x] config_auth_roles_add
* - [x] config_auth_roles_update
* - [x] config_auth_roles_remove
*/
describe("mcp auth", async () => {
let app: App;
let server: McpServer;
beforeEach(async () => {
app = createApp({
config: {
auth: {
enabled: true,
jwt: {
secret: "secret",
},
},
server: {
mcp: {
enabled: true,
},
},
},
});
await app.build();
await app.getMcpClient().ping();
server = app.mcp!;
server.setLogLevel("error");
server.onNotification((message) => {
console.dir(message, { depth: null });
});
});
const tool = createMcpToolCaller();
test("auth_*", async () => {
const me = await tool(server, "auth_me", {});
expect(me.user).toBeNull();
// strategies
const strategies = await tool(server, "auth_strategies", {});
expect(Object.keys(strategies.strategies).length).toEqual(1);
expect(strategies.strategies.password.enabled).toBe(true);
// create user
const user = await tool(
server,
"auth_user_create",
{
email: "test@test.com",
password: "12345678",
},
new Headers(),
);
expect(user.email).toBe("test@test.com");
// create token
const token = await tool(
server,
"auth_user_token",
{
email: "test@test.com",
},
new Headers(),
);
expect(token.token).toBeDefined();
expect(token.user.email).toBe("test@test.com");
// me
const me2 = await tool(
server,
"auth_me",
{},
new Request("http://localhost", {
headers: new Headers({
Authorization: `Bearer ${token.token}`,
}),
}),
);
expect(me2.user.email).toBe("test@test.com");
// change password
const changePassword = await tool(
server,
"auth_user_password_change",
{
email: "test@test.com",
password: "87654321",
},
new Headers(),
);
expect(changePassword.changed).toBe(true);
// test password
const testPassword = await tool(
server,
"auth_user_password_test",
{
email: "test@test.com",
password: "87654321",
},
new Headers(),
);
expect(testPassword.valid).toBe(true);
});
test("config_auth_{get,update}", async () => {
expect(await tool(server, "config_auth_get", {})).toEqual({
path: "",
secrets: false,
partial: false,
value: app.toJSON().auth,
});
// update
await tool(server, "config_auth_update", {
value: {
allow_register: false,
},
});
expect(app.toJSON().auth.allow_register).toBe(false);
});
test("config_auth_strategies_{get,add,update,remove}", async () => {
const strategies = await tool(server, "config_auth_strategies_get", {
key: "password",
});
expect(strategies).toEqual({
secrets: false,
module: "auth",
key: "password",
value: {
enabled: true,
type: "password",
},
});
// add google oauth
const addGoogleOauth = await tool(server, "config_auth_strategies_add", {
key: "google",
value: {
type: "oauth",
enabled: true,
config: {
name: "google",
type: "oidc",
client: {
client_id: "client_id",
client_secret: "client_secret",
},
},
},
return_config: true,
});
expect(addGoogleOauth.config.google.enabled).toBe(true);
expect(app.toJSON().auth.strategies.google?.enabled).toBe(true);
// update (disable) google oauth
await tool(server, "config_auth_strategies_update", {
key: "google",
value: {
enabled: false,
},
});
expect(app.toJSON().auth.strategies.google?.enabled).toBe(false);
// remove google oauth
await tool(server, "config_auth_strategies_remove", {
key: "google",
});
expect(app.toJSON().auth.strategies.google).toBeUndefined();
});
test("config_auth_roles_{get,add,update,remove}", async () => {
// add role
const addGuestRole = await tool(server, "config_auth_roles_add", {
key: "guest",
value: {
permissions: ["read", "write"],
},
return_config: true,
});
expect(addGuestRole.config.guest.permissions).toEqual(["read", "write"]);
// update role
await tool(server, "config_auth_roles_update", {
key: "guest",
value: {
permissions: ["read"],
},
});
expect(app.toJSON().auth.roles?.guest?.permissions).toEqual(["read"]);
// get role
const getGuestRole = await tool(server, "config_auth_roles_get", {
key: "guest",
});
expect(getGuestRole.value.permissions).toEqual(["read"]);
// remove role
await tool(server, "config_auth_roles_remove", {
key: "guest",
});
expect(app.toJSON().auth.roles?.guest).toBeUndefined();
});
});
-44
View File
@@ -1,44 +0,0 @@
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
import { createApp } from "core/test/utils";
import { registries } from "index";
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
beforeAll(() => disableConsoleLog());
afterAll(enableConsoleLog);
describe("mcp", () => {
it("should have tools", async () => {
registries.media.register("local", StorageLocalAdapter);
const app = createApp({
config: {
auth: {
enabled: true,
},
media: {
enabled: true,
adapter: {
type: "local",
config: {
path: "./",
},
},
},
server: {
mcp: {
enabled: true,
},
},
},
});
await app.build();
// expect mcp to not be loaded yet
expect(app.mcp).toBeNull();
// after first request, mcp should be loaded
await app.getMcpClient().listTools();
expect(app.mcp?.tools.length).toBeGreaterThan(0);
});
});
-347
View File
@@ -1,347 +0,0 @@
import { describe, test, expect, beforeEach, beforeAll, afterAll } from "bun:test";
import { type App, createApp, createMcpToolCaller } from "core/test/utils";
import { getSystemMcp } from "modules/mcp/system-mcp";
import { pickKeys, type McpServer } from "bknd/utils";
import { entity, text } from "bknd";
import { disableConsoleLog, enableConsoleLog } from "core/utils";
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
/**
* - [ ] data_sync
* - [x] data_entity_fn_count
* - [x] data_entity_fn_exists
* - [x] data_entity_read_one
* - [x] data_entity_read_many
* - [x] data_entity_insert
* - [x] data_entity_update_many
* - [x] data_entity_update_one
* - [x] data_entity_delete_one
* - [x] data_entity_delete_many
* - [x] data_entity_info
* - [ ] config_data_get
* - [ ] config_data_update
* - [x] config_data_entities_get
* - [x] config_data_entities_add
* - [x] config_data_entities_update
* - [x] config_data_entities_remove
* - [x] config_data_relations_add
* - [x] config_data_relations_get
* - [x] config_data_relations_update
* - [x] config_data_relations_remove
* - [x] config_data_indices_get
* - [x] config_data_indices_add
* - [x] config_data_indices_update
* - [x] config_data_indices_remove
*/
describe("mcp data", async () => {
let app: App;
let server: McpServer;
beforeEach(async () => {
const time = performance.now();
app = createApp({
config: {
server: {
mcp: {
enabled: true,
},
},
},
});
await app.build();
await app.getMcpClient().ping();
server = app.mcp!;
server.setLogLevel("error");
server.onNotification((message) => {
console.dir(message, { depth: null });
});
});
const tool = createMcpToolCaller();
test("config_data_entities_{add,get,update,remove}", async () => {
const result = await tool(server, "config_data_entities_add", {
key: "test",
return_config: true,
value: {},
});
expect(result.success).toBe(true);
expect(result.module).toBe("data");
expect(result.config.test?.type).toEqual("regular");
const entities = Object.keys(app.toJSON().data.entities ?? {});
expect(entities).toContain("test");
{
// get
const result = await tool(server, "config_data_entities_get", {
key: "test",
});
expect(result.module).toBe("data");
expect(result.key).toBe("test");
expect(result.value.type).toEqual("regular");
}
{
// update
const result = await tool(server, "config_data_entities_update", {
key: "test",
return_config: true,
value: {
config: {
name: "Test",
},
},
});
expect(result.success).toBe(true);
expect(result.module).toBe("data");
expect(result.config.test.config?.name).toEqual("Test");
expect(app.toJSON().data.entities?.test?.config?.name).toEqual("Test");
}
{
// remove
const result = await tool(server, "config_data_entities_remove", {
key: "test",
});
expect(result.success).toBe(true);
expect(result.module).toBe("data");
expect(app.toJSON().data.entities?.test).toBeUndefined();
}
});
test("config_data_relations_{add,get,update,remove}", async () => {
// create posts and comments
await tool(server, "config_data_entities_add", {
key: "posts",
value: {},
});
await tool(server, "config_data_entities_add", {
key: "comments",
value: {},
});
expect(Object.keys(app.toJSON().data.entities ?? {})).toEqual(["posts", "comments"]);
// create relation
await tool(server, "config_data_relations_add", {
key: "", // doesn't matter
value: {
type: "n:1",
source: "comments",
target: "posts",
},
});
const config = app.toJSON().data;
expect(
pickKeys((config.relations?.n1_comments_posts as any) ?? {}, ["type", "source", "target"]),
).toEqual({
type: "n:1",
source: "comments",
target: "posts",
});
expect(config.entities?.comments?.fields?.posts_id?.type).toBe("relation");
{
// info
const postsInfo = await tool(server, "data_entity_info", {
entity: "posts",
});
expect(postsInfo.fields).toEqual(["id"]);
expect(postsInfo.relations.all.length).toBe(1);
const commentsInfo = await tool(server, "data_entity_info", {
entity: "comments",
});
expect(commentsInfo.fields).toEqual(["id", "posts_id"]);
expect(commentsInfo.relations.all.length).toBe(1);
}
// update
await tool(server, "config_data_relations_update", {
key: "n1_comments_posts",
value: {
config: {
with_limit: 10,
},
},
});
expect((app.toJSON().data.relations?.n1_comments_posts?.config as any)?.with_limit).toBe(10);
// delete
await tool(server, "config_data_relations_remove", {
key: "n1_comments_posts",
});
expect(app.toJSON().data.relations?.n1_comments_posts).toBeUndefined();
});
test("config_data_indices_update", async () => {
expect(server.tools.map((t) => t.name).includes("config_data_indices_update")).toBe(false);
});
test("config_data_indices_{add,get,remove}", async () => {
// create posts and comments
await tool(server, "config_data_entities_add", {
key: "posts",
value: entity("posts", {
title: text(),
content: text(),
}).toJSON(),
});
// add index on title
await tool(server, "config_data_indices_add", {
key: "", // auto generated
value: {
entity: "posts",
fields: ["title"],
},
});
expect(app.toJSON().data.indices?.idx_posts_title).toEqual({
entity: "posts",
fields: ["title"],
unique: false,
});
// delete
await tool(server, "config_data_indices_remove", {
key: "idx_posts_title",
});
expect(app.toJSON().data.indices?.idx_posts_title).toBeUndefined();
});
test("data_entity_*", async () => {
// create posts and comments
await tool(server, "config_data_entities_add", {
key: "posts",
value: entity("posts", {
title: text(),
content: text(),
}).toJSON(),
});
await tool(server, "config_data_entities_add", {
key: "comments",
value: entity("comments", {
content: text(),
}).toJSON(),
});
// insert a few posts
for (let i = 0; i < 10; i++) {
await tool(server, "data_entity_insert", {
entity: "posts",
json: {
title: `Post ${i}`,
},
});
}
// insert a few comments
for (let i = 0; i < 5; i++) {
await tool(server, "data_entity_insert", {
entity: "comments",
json: {
content: `Comment ${i}`,
},
});
}
const result = await tool(server, "data_entity_read_many", {
entity: "posts",
limit: 5,
});
expect(result.data.length).toBe(5);
expect(result.meta.items).toBe(5);
expect(result.meta.total).toBe(10);
expect(result.data[0].title).toBe("Post 0");
{
// count
const result = await tool(server, "data_entity_fn_count", {
entity: "posts",
});
expect(result.count).toBe(10);
}
{
// exists
const res = await tool(server, "data_entity_fn_exists", {
entity: "posts",
json: {
id: result.data[0].id,
},
});
expect(res.exists).toBe(true);
const res2 = await tool(server, "data_entity_fn_exists", {
entity: "posts",
json: {
id: "123",
},
});
expect(res2.exists).toBe(false);
}
// update
await tool(server, "data_entity_update_one", {
entity: "posts",
id: result.data[0].id,
json: {
title: "Post 0 updated",
},
});
const result2 = await tool(server, "data_entity_read_one", {
entity: "posts",
id: result.data[0].id,
});
expect(result2.data.title).toBe("Post 0 updated");
// delete the second post
await tool(server, "data_entity_delete_one", {
entity: "posts",
id: result.data[1].id,
});
const result3 = await tool(server, "data_entity_read_many", {
entity: "posts",
limit: 2,
});
expect(result3.data.map((p) => p.id)).toEqual([1, 3]);
// update many
await tool(server, "data_entity_update_many", {
entity: "posts",
update: {
title: "Post updated",
},
where: {
title: { $isnull: 0 },
},
});
const result4 = await tool(server, "data_entity_read_many", {
entity: "posts",
limit: 10,
});
expect(result4.data.length).toBe(9);
expect(result4.data.map((p) => p.title)).toEqual(
Array.from({ length: 9 }, () => "Post updated"),
);
// delete many
await tool(server, "data_entity_delete_many", {
entity: "posts",
json: {
title: { $isnull: 0 },
},
});
const result5 = await tool(server, "data_entity_read_many", {
entity: "posts",
limit: 10,
});
expect(result5.data.length).toBe(0);
expect(result5.meta.items).toBe(0);
expect(result5.meta.total).toBe(0);
});
});
-119
View File
@@ -1,119 +0,0 @@
import { describe, test, expect, beforeAll, beforeEach, afterAll } from "bun:test";
import { type App, createApp, createMcpToolCaller } from "core/test/utils";
import { getSystemMcp } from "modules/mcp/system-mcp";
import { registries } from "index";
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
import { disableConsoleLog, enableConsoleLog } from "core/utils";
import type { McpServer } from "bknd/utils";
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
/**
* - [x] config_media_get
* - [x] config_media_update
* - [x] config_media_adapter_get
* - [x] config_media_adapter_update
*/
describe("mcp media", async () => {
let app: App;
let server: McpServer;
beforeEach(async () => {
registries.media.register("local", StorageLocalAdapter);
app = createApp({
config: {
media: {
enabled: true,
adapter: {
type: "local",
config: {
path: "./",
},
},
},
server: {
mcp: {
enabled: true,
},
},
},
});
await app.build();
await app.getMcpClient().ping();
server = app.mcp!;
server.setLogLevel("error");
server.onNotification((message) => {
console.dir(message, { depth: null });
});
});
const tool = createMcpToolCaller();
test("config_media_{get,update}", async () => {
const result = await tool(server, "config_media_get", {});
expect(result).toEqual({
path: "",
secrets: false,
partial: false,
value: app.toJSON().media,
});
// partial
expect((await tool(server, "config_media_get", { path: "adapter" })).value).toEqual({
type: "local",
config: {
path: "./",
},
});
// update
await tool(server, "config_media_update", {
value: {
storage: {
body_max_size: 1024 * 1024 * 10,
},
},
return_config: true,
});
expect(app.toJSON().media.storage.body_max_size).toBe(1024 * 1024 * 10);
});
test("config_media_adapter_{get,update}", async () => {
const result = await tool(server, "config_media_adapter_get", {});
expect(result).toEqual({
secrets: false,
value: app.toJSON().media.adapter,
});
// update
await tool(server, "config_media_adapter_update", {
value: {
type: "local",
config: {
path: "./subdir",
},
},
});
const adapter = app.toJSON().media.adapter as any;
expect(adapter.config.path).toBe("./subdir");
expect(adapter.type).toBe("local");
// set to s3
{
await tool(server, "config_media_adapter_update", {
value: {
type: "s3",
config: {
access_key: "123",
secret_access_key: "456",
url: "https://example.com/what",
},
},
});
const adapter = app.toJSON(true).media.adapter as any;
expect(adapter.type).toBe("s3");
expect(adapter.config.url).toBe("https://example.com/what");
}
});
});
-77
View File
@@ -1,77 +0,0 @@
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
import { type App, createApp, createMcpToolCaller } from "core/test/utils";
import type { McpServer } from "bknd/utils";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
beforeAll(() => disableConsoleLog());
afterAll(enableConsoleLog);
/**
* - [x] config_server_get
* - [x] config_server_update
*/
describe("mcp system", async () => {
let app: App;
let server: McpServer;
beforeAll(async () => {
app = createApp({
config: {
server: {
mcp: {
enabled: true,
},
},
},
});
await app.build();
await app.getMcpClient().ping();
server = app.mcp!;
});
const tool = createMcpToolCaller();
test("config_server_get", async () => {
const result = await tool(server, "config_server_get", {});
expect(JSON.parse(JSON.stringify(result))).toEqual({
path: "",
secrets: false,
partial: false,
value: JSON.parse(JSON.stringify(app.toJSON().server)),
});
});
test("config_server_get2", async () => {
const result = await tool(server, "config_server_get", {});
expect(JSON.parse(JSON.stringify(result))).toEqual({
path: "",
secrets: false,
partial: false,
value: JSON.parse(JSON.stringify(app.toJSON().server)),
});
});
test("config_server_update", async () => {
const original = JSON.parse(JSON.stringify(app.toJSON().server));
const result = await tool(server, "config_server_update", {
value: {
cors: {
origin: "http://localhost",
},
},
return_config: true,
});
expect(JSON.parse(JSON.stringify(result))).toEqual({
success: true,
module: "server",
config: {
...original,
cors: {
...original.cors,
origin: "http://localhost",
},
},
});
expect(app.toJSON().server.cors.origin).toBe("http://localhost");
});
});
-57
View File
@@ -1,57 +0,0 @@
import { AppEvents } from "App";
import { describe, test, expect, beforeAll, mock } from "bun:test";
import { type App, createApp, createMcpToolCaller } from "core/test/utils";
import type { McpServer } from "bknd/utils";
/**
* - [x] system_config
* - [x] system_build
* - [x] system_ping
* - [x] system_info
*/
describe("mcp system", async () => {
let app: App;
let server: McpServer;
beforeAll(async () => {
app = createApp({
config: {
server: {
mcp: {
enabled: true,
},
},
},
});
await app.build();
await app.getMcpClient().ping();
server = app.mcp!;
});
const tool = createMcpToolCaller();
test("system_ping", async () => {
const result = await tool(server, "system_ping", {});
expect(result).toEqual({ pong: true });
});
test("system_info", async () => {
const result = await tool(server, "system_info", {});
expect(Object.keys(result).length).toBeGreaterThan(0);
expect(Object.keys(result)).toContainValues(["version", "runtime", "connection"]);
});
test("system_build", async () => {
const called = mock(() => null);
app.emgr.onEvent(AppEvents.AppBuiltEvent, () => void called(), { once: true });
const result = await tool(server, "system_build", {});
expect(called).toHaveBeenCalledTimes(1);
expect(result.success).toBe(true);
});
test("system_config", async () => {
const result = await tool(server, "system_config", {});
expect(result).toEqual(app.toJSON());
});
});
@@ -1,80 +0,0 @@
import { describe, it, expect, mock, beforeAll, afterAll } from "bun:test";
import { createApp } from "core/test/utils";
import { syncConfig } from "plugins/dev/sync-config.plugin";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
beforeAll(() => disableConsoleLog());
afterAll(enableConsoleLog);
describe("syncConfig", () => {
it("should only sync if enabled", async () => {
const called = mock(() => null);
const app = createApp();
await app.build();
await syncConfig({
write: () => {
called();
},
enabled: false,
includeFirstBoot: false,
})(app).onBuilt?.();
expect(called).not.toHaveBeenCalled();
await syncConfig({
write: () => {
called();
},
enabled: false,
includeFirstBoot: true,
})(app).onBuilt?.();
expect(called).not.toHaveBeenCalled();
await syncConfig({
write: () => {
called();
},
enabled: true,
includeFirstBoot: true,
})(app).onBuilt?.();
expect(called).toHaveBeenCalledTimes(1);
});
it("should respect secrets", async () => {
const called = mock(() => null);
const app = createApp({
config: {
auth: {
enabled: true,
jwt: {
secret: "test",
},
},
},
});
await app.build();
await syncConfig({
write: async (config) => {
called();
expect(config.auth.jwt.secret).toBe("test");
},
enabled: true,
includeSecrets: true,
includeFirstBoot: true,
})(app).onBuilt?.();
await syncConfig({
write: async (config) => {
called();
// it's an important test, because the `jwt` part is omitted if secrets=false in general app.toJSON()
// but it's required to get the app running
expect(config.auth.jwt.secret).toBe("");
},
enabled: true,
includeSecrets: false,
includeFirstBoot: true,
})(app).onBuilt?.();
expect(called).toHaveBeenCalledTimes(2);
});
});
+3 -8
View File
@@ -1,12 +1,7 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { registries } from "../../src";
import { createApp } from "core/test/utils"; import { createApp } from "core/test/utils";
import * as proto from "../../src/data/prototype"; import * as proto from "../../src/data/prototype";
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter"; import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
beforeAll(() => disableConsoleLog());
afterAll(enableConsoleLog);
describe("repros", async () => { describe("repros", async () => {
/** /**
@@ -18,8 +13,8 @@ describe("repros", async () => {
* There was an issue that AppData had old configs because of system entity "media" * There was an issue that AppData had old configs because of system entity "media"
*/ */
test("registers media entity correctly to relate to it", async () => { test("registers media entity correctly to relate to it", async () => {
registries.media.register("local", StorageLocalAdapter);
const app = createApp(); const app = createApp();
app.module.media.adapters.set("local", StorageLocalAdapter);
await app.build(); await app.build();
{ {
@@ -92,7 +87,7 @@ describe("repros", async () => {
fns.relation(schema.product_likes).manyToOne(schema.users); fns.relation(schema.product_likes).manyToOne(schema.users);
}, },
); );
const app = createApp({ config: { data: schema.toJSON() } }); const app = createApp({ initialConfig: { data: schema.toJSON() } });
await app.build(); await app.build();
const info = (await (await app.server.request("/api/data/info/products")).json()) as any; const info = (await (await app.server.request("/api/data/info/products")).json()) as any;
+43 -1
View File
@@ -1,3 +1,45 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { Authenticator, type User, type UserPool } from "../../src/auth";
import { cookieConfig } from "../../src/auth/authenticate/Authenticator";
import { PasswordStrategy } from "../../src/auth/authenticate/strategies/PasswordStrategy";
import { parse } from "core/object/schema";
describe("Authenticator", async () => {}); /*class MemoryUserPool implements UserPool {
constructor(private users: User[] = []) {}
async findBy(prop: "id" | "email" | "username", value: string | number) {
return this.users.find((user) => user[prop] === value);
}
async create(user: Pick<User, "email" | "password">) {
const id = this.users.length + 1;
const newUser = { ...user, id, username: user.email };
this.users.push(newUser);
return newUser;
}
}*/
describe("Authenticator", async () => {
test("cookie options", async () => {
console.log("parsed", parse(cookieConfig, undefined));
console.log(cookieConfig.template({}));
});
/*const userpool = new MemoryUserPool([
{ id: 1, email: "d", username: "test", password: await hash.sha256("test") },
]);
test("sha256 login", async () => {
const auth = new Authenticator(userpool, {
password: new PasswordStrategy({
hashing: "sha256",
}),
});
const { token } = await auth.login("password", { email: "d", password: "test" });
expect(token).toBeDefined();
const { iat, ...decoded } = decodeJwt<any>(token);
expect(decoded).toEqual({ id: 1, email: "d", username: "test" });
expect(await auth.verify(token)).toBe(true);
});*/
});
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { Guard } from "../../../src/auth/authorize/Guard"; import { Guard } from "../../../src/auth";
describe("authorize", () => { describe("authorize", () => {
test("basic", async () => { test("basic", async () => {
+1 -1
View File
@@ -1,6 +1,6 @@
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
import { Event, EventManager, InvalidEventReturn, NoParamEvent } from "../../src/core/events"; import { Event, EventManager, InvalidEventReturn, NoParamEvent } from "../../src/core/events";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test"; import { disableConsoleLog, enableConsoleLog } from "../helper";
beforeAll(disableConsoleLog); beforeAll(disableConsoleLog);
afterAll(enableConsoleLog); afterAll(enableConsoleLog);
+2 -2
View File
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { Registry } from "core/registry/Registry"; import { Registry } from "core";
import { s } from "core/utils/schema"; import { s } from "core/object/schema";
type Constructor<T> = new (...args: any[]) => T; type Constructor<T> = new (...args: any[]) => T;
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { s } from "core/utils/schema"; import { SchemaObject } from "../../../src/core";
import { SchemaObject } from "core/object/SchemaObject"; import { s } from "core/object/schema";
describe("SchemaObject", async () => { describe("SchemaObject", async () => {
test("basic", async () => { test("basic", async () => {
+4 -4
View File
@@ -248,7 +248,7 @@ describe("Core Utils", async () => {
expect(utils.getContentName(request)).toBe(name); expect(utils.getContentName(request)).toBe(name);
}); });
test("detectImageDimensions", async () => { test.only("detectImageDimensions", async () => {
// wrong // wrong
// @ts-expect-error // @ts-expect-error
expect(utils.detectImageDimensions(new ArrayBuffer(), "text/plain")).rejects.toThrow(); expect(utils.detectImageDimensions(new ArrayBuffer(), "text/plain")).rejects.toThrow();
@@ -267,12 +267,12 @@ describe("Core Utils", async () => {
}); });
describe("dates", () => { describe("dates", () => {
test("formats local time", () => { test.only("formats local time", () => {
expect(utils.datetimeStringUTC("2025-02-21T16:48:25.841Z")).toBe("2025-02-21 16:48:25"); expect(utils.datetimeStringUTC("2025-02-21T16:48:25.841Z")).toBe("2025-02-21 16:48:25");
/*console.log(utils.datetimeStringUTC(new Date())); console.log(utils.datetimeStringUTC(new Date()));
console.log(utils.datetimeStringUTC()); console.log(utils.datetimeStringUTC());
console.log(new Date()); console.log(new Date());
console.log("timezone", Intl.DateTimeFormat().resolvedOptions().timeZone); */ console.log("timezone", Intl.DateTimeFormat().resolvedOptions().timeZone);
}); });
}); });
}); });
+11 -9
View File
@@ -1,20 +1,22 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { Guard } from "../../src/auth/authorize/Guard"; import { Guard } from "../../src/auth";
import { parse } from "core/utils/schema"; import { parse } from "core/object/schema";
import {
Entity,
type EntityData,
EntityManager,
ManyToOneRelation,
TextField,
} from "../../src/data";
import { DataController } from "../../src/data/api/DataController"; import { DataController } from "../../src/data/api/DataController";
import { dataConfigSchema } from "../../src/data/data-schema"; import { dataConfigSchema } from "../../src/data/data-schema";
import { getDummyConnection } from "../helper"; import { disableConsoleLog, enableConsoleLog, getDummyConnection } from "../helper";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
import type { RepositoryResultJSON } from "data/entities/query/RepositoryResult"; import type { RepositoryResultJSON } from "data/entities/query/RepositoryResult";
import type { MutatorResultJSON } from "data/entities/mutation/MutatorResult"; import type { MutatorResultJSON } from "data/entities/mutation/MutatorResult";
import { Entity, EntityManager, type EntityData } from "data/entities";
import { TextField } from "data/fields";
import { ManyToOneRelation } from "data/relations";
const { dummyConnection, afterAllCleanup } = getDummyConnection(); const { dummyConnection, afterAllCleanup } = getDummyConnection();
beforeAll(() => disableConsoleLog()); beforeAll(() => disableConsoleLog(["log", "warn"]));
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog()); afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
const dataConfig = parse(dataConfigSchema, {}); const dataConfig = parse(dataConfigSchema, {});
+8 -2
View File
@@ -1,6 +1,12 @@
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import { Entity, EntityManager } from "data/entities"; import {
import { TextField, PrimaryField, NumberField } from "data/fields"; Entity,
EntityManager,
NumberField,
PrimaryField,
Repository,
TextField,
} from "../../src/data";
import { getDummyConnection } from "./helper"; import { getDummyConnection } from "./helper";
const { dummyConnection, afterAllCleanup } = getDummyConnection(); const { dummyConnection, afterAllCleanup } = getDummyConnection();
+1 -1
View File
@@ -2,7 +2,7 @@ import { unlink } from "node:fs/promises";
import type { SqliteDatabase } from "kysely"; import type { SqliteDatabase } from "kysely";
// @ts-ignore // @ts-ignore
import Database from "libsql"; import Database from "libsql";
import { SqliteLocalConnection } from "data/connection/sqlite/SqliteLocalConnection"; import { SqliteLocalConnection } from "../../src/data";
export function getDummyDatabase(memory: boolean = true): { export function getDummyDatabase(memory: boolean = true): {
dummyDb: SqliteDatabase; dummyDb: SqliteDatabase;
+8 -5
View File
@@ -1,10 +1,13 @@
// eslint-disable-next-line import/no-unresolved // eslint-disable-next-line import/no-unresolved
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import { Entity } from "data/entities"; import {
import { EntityManager } from "data/entities/EntityManager"; Entity,
import { ManyToOneRelation } from "data/relations"; EntityManager,
import { NumberField, TextField } from "data/fields"; ManyToOneRelation,
import { SchemaManager } from "data/schema/SchemaManager"; NumberField,
SchemaManager,
TextField,
} from "../../src/data";
import { getDummyConnection } from "./helper"; import { getDummyConnection } from "./helper";
const { dummyConnection, afterAllCleanup } = getDummyConnection(); const { dummyConnection, afterAllCleanup } = getDummyConnection();
+2 -3
View File
@@ -1,8 +1,7 @@
// eslint-disable-next-line import/no-unresolved // eslint-disable-next-line import/no-unresolved
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import { Entity, EntityManager } from "data/entities"; import { Entity, EntityManager, Mutator, NumberField, TextField } from "../../src/data";
import { NumberField, TextField } from "data/fields"; import { TransformPersistFailedException } from "../../src/data/errors";
import { TransformPersistFailedException } from "data/errors";
import { getDummyConnection } from "./helper"; import { getDummyConnection } from "./helper";
const { dummyConnection, afterAllCleanup } = getDummyConnection(); const { dummyConnection, afterAllCleanup } = getDummyConnection();
+2 -4
View File
@@ -1,8 +1,6 @@
import { afterAll, expect as bunExpect, describe, test } from "bun:test"; import { afterAll, expect as bunExpect, describe, test } from "bun:test";
import { stripMark } from "core/utils/schema"; import { stripMark } from "core/object/schema";
import { Entity, EntityManager } from "data/entities"; import { Entity, EntityManager, PolymorphicRelation, TextField } from "../../src/data";
import { TextField } from "data/fields";
import { PolymorphicRelation } from "data/relations";
import { getDummyConnection } from "./helper"; import { getDummyConnection } from "./helper";
const { dummyConnection, afterAllCleanup } = getDummyConnection(); const { dummyConnection, afterAllCleanup } = getDummyConnection();
+11 -12
View File
@@ -2,20 +2,19 @@ import { describe, expect, test } from "bun:test";
import { import {
BooleanField, BooleanField,
DateField, DateField,
Entity,
EntityIndex,
EntityManager,
EnumField, EnumField,
JsonField, JsonField,
NumberField,
TextField,
EntityIndex,
} from "data/fields";
import { Entity, EntityManager } from "data/entities";
import {
ManyToManyRelation, ManyToManyRelation,
ManyToOneRelation, ManyToOneRelation,
NumberField,
OneToOneRelation, OneToOneRelation,
PolymorphicRelation, PolymorphicRelation,
} from "data/relations"; TextField,
import { DummyConnection } from "data/connection/DummyConnection"; } from "../../src/data";
import { DummyConnection } from "../../src/data/connection/DummyConnection";
import { import {
FieldPrototype, FieldPrototype,
type FieldSchema, type FieldSchema,
@@ -33,8 +32,8 @@ import {
number, number,
relation, relation,
text, text,
} from "data/prototype"; } from "../../src/data/prototype";
import { MediaField } from "media/MediaField"; import { MediaField } from "../../src/media/MediaField";
describe("prototype", () => { describe("prototype", () => {
test("...", () => { test("...", () => {
@@ -297,9 +296,9 @@ describe("prototype", () => {
new Entity("posts", [new TextField("name"), new TextField("slug", { required: true })]), new Entity("posts", [new TextField("name"), new TextField("slug", { required: true })]),
new Entity("comments", [new TextField("some")]), new Entity("comments", [new TextField("some")]),
new Entity("users", [new TextField("email")]), new Entity("users", [new TextField("email")]),
] as const; ];
const _em2 = new EntityManager( const _em2 = new EntityManager(
[...es], es,
new DummyConnection(), new DummyConnection(),
[new ManyToOneRelation(es[0], es[1]), new ManyToOneRelation(es[0], es[2])], [new ManyToOneRelation(es[0], es[1]), new ManyToOneRelation(es[0], es[2])],
[ [
+10 -11
View File
@@ -1,14 +1,13 @@
// eslint-disable-next-line import/no-unresolved // eslint-disable-next-line import/no-unresolved
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import { Entity, EntityManager } from "data/entities"; import { Entity, EntityManager, TextField } from "../../src/data";
import { TextField } from "data/fields";
import { import {
ManyToManyRelation, ManyToManyRelation,
ManyToOneRelation, ManyToOneRelation,
OneToOneRelation, OneToOneRelation,
PolymorphicRelation, PolymorphicRelation,
RelationField, RelationField,
} from "data/relations"; } from "../../src/data/relations";
import { getDummyConnection } from "./helper"; import { getDummyConnection } from "./helper";
const { dummyConnection, afterAllCleanup } = getDummyConnection(); const { dummyConnection, afterAllCleanup } = getDummyConnection();
@@ -78,7 +77,7 @@ describe("Relations", async () => {
const em = new EntityManager(entities, dummyConnection, relations); const em = new EntityManager(entities, dummyConnection, relations);
// verify naming // verify naming
const rel = em.relations.all[0]!; const rel = em.relations.all[0];
expect(rel.source.entity.name).toBe(posts.name); expect(rel.source.entity.name).toBe(posts.name);
expect(rel.source.reference).toBe(posts.name); expect(rel.source.reference).toBe(posts.name);
expect(rel.target.entity.name).toBe(users.name); expect(rel.target.entity.name).toBe(users.name);
@@ -90,11 +89,11 @@ describe("Relations", async () => {
// verify low level relation // verify low level relation
expect(em.relationsOf(users.name).length).toBe(1); expect(em.relationsOf(users.name).length).toBe(1);
expect(em.relationsOf(users.name).length).toBe(1); expect(em.relationsOf(users.name).length).toBe(1);
expect(em.relationsOf(users.name)[0]!.source.entity).toBe(posts); expect(em.relationsOf(users.name)[0].source.entity).toBe(posts);
expect(posts.field("author_id")).toBeInstanceOf(RelationField); expect(posts.field("author_id")).toBeInstanceOf(RelationField);
expect(em.relationsOf(users.name).length).toBe(1); expect(em.relationsOf(users.name).length).toBe(1);
expect(em.relationsOf(users.name).length).toBe(1); expect(em.relationsOf(users.name).length).toBe(1);
expect(em.relationsOf(users.name)[0]!.source.entity).toBe(posts); expect(em.relationsOf(users.name)[0].source.entity).toBe(posts);
// verify high level relation (from users) // verify high level relation (from users)
const userPostsRel = em.relationOf(users.name, "posts"); const userPostsRel = em.relationOf(users.name, "posts");
@@ -192,7 +191,7 @@ describe("Relations", async () => {
const em = new EntityManager(entities, dummyConnection, relations); const em = new EntityManager(entities, dummyConnection, relations);
// verify naming // verify naming
const rel = em.relations.all[0]!; const rel = em.relations.all[0];
expect(rel.source.entity.name).toBe(users.name); expect(rel.source.entity.name).toBe(users.name);
expect(rel.source.reference).toBe(users.name); expect(rel.source.reference).toBe(users.name);
expect(rel.target.entity.name).toBe(settings.name); expect(rel.target.entity.name).toBe(settings.name);
@@ -203,8 +202,8 @@ describe("Relations", async () => {
expect(em.relationsOf(users.name).length).toBe(1); expect(em.relationsOf(users.name).length).toBe(1);
expect(em.relationsOf(users.name).length).toBe(1); expect(em.relationsOf(users.name).length).toBe(1);
expect(em.relationsOf(users.name)[0]!.source.entity).toBe(users); expect(em.relationsOf(users.name)[0].source.entity).toBe(users);
expect(em.relationsOf(users.name)[0]!.target.entity).toBe(settings); expect(em.relationsOf(users.name)[0].target.entity).toBe(settings);
// verify high level relation (from users) // verify high level relation (from users)
const userSettingRel = em.relationOf(users.name, settings.name); const userSettingRel = em.relationOf(users.name, settings.name);
@@ -324,7 +323,7 @@ describe("Relations", async () => {
); );
// mutation info // mutation info
expect(relations[0]!.helper(posts.name)!.getMutationInfo()).toEqual({ expect(relations[0].helper(posts.name)!.getMutationInfo()).toEqual({
reference: "categories", reference: "categories",
local_field: undefined, local_field: undefined,
$set: false, $set: false,
@@ -335,7 +334,7 @@ describe("Relations", async () => {
cardinality: undefined, cardinality: undefined,
relation_type: "m:n", relation_type: "m:n",
}); });
expect(relations[0]!.helper(categories.name)!.getMutationInfo()).toEqual({ expect(relations[0].helper(categories.name)!.getMutationInfo()).toEqual({
reference: "posts", reference: "posts",
local_field: undefined, local_field: undefined,
$set: false, $set: false,
+6 -2
View File
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { Entity } from "data/entities"; import { Entity, NumberField, TextField } from "data";
import { NumberField, TextField } from "data/fields"; import * as p from "data/prototype";
describe("[data] Entity", async () => { describe("[data] Entity", async () => {
const entity = new Entity("test", [ const entity = new Entity("test", [
@@ -47,4 +47,8 @@ describe("[data] Entity", async () => {
entity.addField(field); entity.addField(field);
expect(entity.getField("new_field")).toBe(field); expect(entity.getField("new_field")).toBe(field);
}); });
test.only("types", async () => {
console.log(entity.toTypes());
});
}); });
@@ -1,8 +1,12 @@
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import { Entity, EntityManager } from "data/entities"; import {
import { ManyToManyRelation, ManyToOneRelation } from "data/relations"; Entity,
import { SchemaManager } from "data/schema/SchemaManager"; EntityManager,
import { UnableToConnectException } from "data/errors"; ManyToManyRelation,
ManyToOneRelation,
SchemaManager,
} from "../../../src/data";
import { UnableToConnectException } from "../../../src/data/errors";
import { getDummyConnection } from "../helper"; import { getDummyConnection } from "../helper";
const { dummyConnection, afterAllCleanup } = getDummyConnection(); const { dummyConnection, afterAllCleanup } = getDummyConnection();
+4 -9
View File
@@ -1,15 +1,10 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import { Entity, EntityManager } from "data/entities"; import { Entity, EntityManager, ManyToOneRelation, TextField } from "../../../src/data";
import { ManyToOneRelation } from "data/relations"; import { JoinBuilder } from "../../../src/data/entities/query/JoinBuilder";
import { TextField } from "data/fields";
import { JoinBuilder } from "data/entities/query/JoinBuilder";
import { getDummyConnection } from "../helper"; import { getDummyConnection } from "../helper";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
beforeAll(() => disableConsoleLog());
const { dummyConnection, afterAllCleanup } = getDummyConnection(); const { dummyConnection, afterAllCleanup } = getDummyConnection();
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog()); afterAll(afterAllCleanup);
describe("[data] JoinBuilder", async () => { describe("[data] JoinBuilder", async () => {
test("missing relation", async () => { test("missing relation", async () => {
+10 -9
View File
@@ -1,22 +1,23 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import type { EventManager } from "../../../src/core/events"; import type { EventManager } from "../../../src/core/events";
import { Entity, EntityManager } from "data/entities";
import { import {
Entity,
EntityManager,
ManyToOneRelation, ManyToOneRelation,
MutatorEvents,
NumberField,
OneToOneRelation, OneToOneRelation,
RelationField, type RelationField,
RelationMutator, RelationMutator,
} from "data/relations"; TextField,
import { NumberField, TextField } from "data/fields"; } from "../../../src/data";
import * as proto from "data/prototype"; import * as proto from "../../../src/data/prototype";
import { getDummyConnection } from "../../helper"; import { getDummyConnection, disableConsoleLog, enableConsoleLog } from "../../helper";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
import { MutatorEvents } from "data/events";
const { dummyConnection, afterAllCleanup } = getDummyConnection(); const { dummyConnection, afterAllCleanup } = getDummyConnection();
afterAll(afterAllCleanup); afterAll(afterAllCleanup);
beforeAll(() => disableConsoleLog()); beforeAll(() => disableConsoleLog(["log", "warn"]));
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog()); afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
describe("[data] Mutator (base)", async () => { describe("[data] Mutator (base)", async () => {
+14 -9
View File
@@ -1,18 +1,23 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import type { Kysely, Transaction } from "kysely"; import type { Kysely, Transaction } from "kysely";
import { TextField } from "data/fields"; import { Perf } from "core/utils";
import { em as $em, entity as $entity, text as $text } from "data/prototype"; import {
import { Entity, EntityManager } from "data/entities"; Entity,
import { ManyToOneRelation } from "data/relations"; EntityManager,
import { RepositoryEvents } from "data/events"; LibsqlConnection,
ManyToOneRelation,
RepositoryEvents,
TextField,
entity as $entity,
text as $text,
em as $em,
} from "data";
import { getDummyConnection } from "../helper"; import { getDummyConnection } from "../helper";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
type E = Kysely<any> | Transaction<any>; type E = Kysely<any> | Transaction<any>;
const { dummyConnection, afterAllCleanup } = getDummyConnection(); const { dummyConnection, afterAllCleanup } = getDummyConnection();
beforeAll(() => disableConsoleLog()); afterAll(afterAllCleanup);
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
async function sleep(ms: number) { async function sleep(ms: number) {
return new Promise((resolve) => { return new Promise((resolve) => {
@@ -1,9 +1,7 @@
// eslint-disable-next-line import/no-unresolved // eslint-disable-next-line import/no-unresolved
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import { randomString } from "core/utils"; import { randomString } from "../../../src/core/utils";
import { Entity, EntityManager } from "data/entities"; import { Entity, EntityIndex, EntityManager, SchemaManager, TextField } from "../../../src/data";
import { TextField, EntityIndex } from "data/fields";
import { SchemaManager } from "data/schema/SchemaManager";
import { getDummyConnection } from "../helper"; import { getDummyConnection } from "../helper";
const { dummyConnection, afterAllCleanup } = getDummyConnection(); const { dummyConnection, afterAllCleanup } = getDummyConnection();
+1 -1
View File
@@ -1,6 +1,6 @@
import { describe, test, expect } from "bun:test"; import { describe, test, expect } from "bun:test";
import { getDummyConnection } from "../helper"; import { getDummyConnection } from "../helper";
import { WhereBuilder, type WhereQuery } from "data/entities/query/WhereBuilder"; import { type WhereQuery, WhereBuilder } from "data";
function qb() { function qb() {
const c = getDummyConnection(); const c = getDummyConnection();
+11 -10
View File
@@ -1,15 +1,16 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { Entity, EntityManager } from "data/entities"; import {
import { ManyToManyRelation, ManyToOneRelation, PolymorphicRelation } from "data/relations"; Entity,
import { TextField } from "data/fields"; EntityManager,
import * as proto from "data/prototype"; ManyToManyRelation,
import { WithBuilder } from "data/entities/query/WithBuilder"; ManyToOneRelation,
PolymorphicRelation,
TextField,
WithBuilder,
} from "../../../src/data";
import * as proto from "../../../src/data/prototype";
import { schemaToEm } from "../../helper"; import { schemaToEm } from "../../helper";
import { getDummyConnection } from "../helper"; import { getDummyConnection } from "../helper";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
beforeAll(() => disableConsoleLog());
afterAll(enableConsoleLog);
const { dummyConnection } = getDummyConnection(); const { dummyConnection } = getDummyConnection();
@@ -1,5 +1,5 @@
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import { EntityManager } from "data/entities/EntityManager"; import { EntityManager } from "../../../../src/data";
import { getDummyConnection } from "../../helper"; import { getDummyConnection } from "../../helper";
const { dummyConnection, afterAllCleanup } = getDummyConnection(); const { dummyConnection, afterAllCleanup } = getDummyConnection();
@@ -1,10 +1,9 @@
import { bunTestRunner } from "adapter/bun/test";
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { BooleanField } from "data/fields"; import { BooleanField } from "../../../../src/data";
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite"; import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
describe("[data] BooleanField", async () => { describe("[data] BooleanField", async () => {
fieldTestSuite(bunTestRunner, BooleanField, { defaultValue: true, schemaType: "boolean" }); fieldTestSuite({ expect, test }, BooleanField, { defaultValue: true, schemaType: "boolean" });
test("transformRetrieve", async () => { test("transformRetrieve", async () => {
const field = new BooleanField("test"); const field = new BooleanField("test");
@@ -1,5 +1,5 @@
import { describe, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { DateField } from "data/fields"; import { DateField, dateFieldConfigSchema } from "../../../../src/data";
import { fieldTestSuite } from "data/fields/field-test-suite"; import { fieldTestSuite } from "data/fields/field-test-suite";
import { bunTestRunner } from "adapter/bun/test"; import { bunTestRunner } from "adapter/bun/test";
@@ -1,6 +1,5 @@
import { bunTestRunner } from "adapter/bun/test";
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { EnumField } from "data/fields"; import { EnumField } from "../../../../src/data";
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite"; import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
function options(strings: string[]) { function options(strings: string[]) {
@@ -9,7 +8,7 @@ function options(strings: string[]) {
describe("[data] EnumField", async () => { describe("[data] EnumField", async () => {
fieldTestSuite( fieldTestSuite(
bunTestRunner, { expect, test },
// @ts-ignore // @ts-ignore
EnumField, EnumField,
{ defaultValue: "a", schemaType: "text" }, { defaultValue: "a", schemaType: "text" },
+1 -1
View File
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test";
import { baseFieldConfigSchema, Field } from "../../../../src/data/fields/Field"; import { baseFieldConfigSchema, Field } from "../../../../src/data/fields/Field";
import { fieldTestSuite } from "data/fields/field-test-suite"; import { fieldTestSuite } from "data/fields/field-test-suite";
import { bunTestRunner } from "adapter/bun/test"; import { bunTestRunner } from "adapter/bun/test";
import { stripMark } from "core/utils/schema"; import { stripMark } from "core/object/schema";
describe("[data] Field", async () => { describe("[data] Field", async () => {
class FieldSpec extends Field { class FieldSpec extends Field {
@@ -1,7 +1,6 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { Entity } from "data/entities"; import { Entity, EntityIndex, Field } from "../../../../src/data";
import { Field, EntityIndex } from "data/fields"; import { s } from "core/object/schema";
import { s } from "core/utils/schema";
class TestField extends Field { class TestField extends Field {
protected getSchema(): any { protected getSchema(): any {
@@ -23,4 +22,11 @@ describe("FieldIndex", async () => {
expect(index.name).toEqual("idx_test_name"); expect(index.name).toEqual("idx_test_name");
expect(index.unique).toEqual(false); expect(index.unique).toEqual(false);
}); });
test("it fails on non-unique", async () => {
const field = new TestField("name", { required: false });
expect(() => new EntityIndex(entity, [field], true)).toThrowError();
expect(() => new EntityIndex(entity, [field])).toBeDefined();
});
}); });
@@ -1,11 +1,10 @@
import { bunTestRunner } from "adapter/bun/test";
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { JsonField } from "data/fields"; import { JsonField } from "../../../../src/data";
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite"; import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
describe("[data] JsonField", async () => { describe("[data] JsonField", async () => {
const field = new JsonField("test"); const field = new JsonField("test");
fieldTestSuite(bunTestRunner, JsonField, { fieldTestSuite({ expect, test }, JsonField, {
defaultValue: { a: 1 }, defaultValue: { a: 1 },
sampleValues: ["string", { test: 1 }, 1], sampleValues: ["string", { test: 1 }, 1],
schemaType: "text", schemaType: "text",
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { JsonSchemaField } from "data/fields"; import { JsonSchemaField } from "../../../../src/data";
import { fieldTestSuite } from "data/fields/field-test-suite"; import { fieldTestSuite } from "data/fields/field-test-suite";
describe("[data] JsonSchemaField", async () => { describe("[data] JsonSchemaField", async () => {
@@ -1,6 +1,5 @@
import { bunTestRunner } from "adapter/bun/test";
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { NumberField } from "data/fields"; import { NumberField } from "../../../../src/data";
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite"; import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
describe("[data] NumberField", async () => { describe("[data] NumberField", async () => {
@@ -16,5 +15,5 @@ describe("[data] NumberField", async () => {
expect(transformPersist(field2, 10000)).resolves.toBe(10000); expect(transformPersist(field2, 10000)).resolves.toBe(10000);
}); });
fieldTestSuite(bunTestRunner, NumberField, { defaultValue: 12, schemaType: "integer" }); fieldTestSuite({ expect, test }, NumberField, { defaultValue: 12, schemaType: "integer" });
}); });
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { PrimaryField } from "data/fields"; import { PrimaryField } from "../../../../src/data";
describe("[data] PrimaryField", async () => { describe("[data] PrimaryField", async () => {
const field = new PrimaryField("primary"); const field = new PrimaryField("primary");
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { TextField } from "data/fields"; import { TextField, textFieldConfigSchema } from "../../../../src/data";
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite"; import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
import { bunTestRunner } from "adapter/bun/test"; import { bunTestRunner } from "adapter/bun/test";
@@ -1,13 +1,11 @@
import { describe, expect, it, test } from "bun:test"; import { describe, expect, it, test } from "bun:test";
import { Entity, type EntityManager } from "data/entities"; import { Entity, type EntityManager } from "../../../../src/data";
import { import {
type BaseRelationConfig, type BaseRelationConfig,
EntityRelation, EntityRelation,
EntityRelationAnchor, EntityRelationAnchor,
ManyToManyRelation,
RelationTypes, RelationTypes,
} from "data/relations"; } from "../../../../src/data/relations";
import * as proto from "data/prototype";
class TestEntityRelation extends EntityRelation { class TestEntityRelation extends EntityRelation {
constructor(config?: BaseRelationConfig) { constructor(config?: BaseRelationConfig) {
@@ -26,11 +24,11 @@ class TestEntityRelation extends EntityRelation {
return this; return this;
} }
buildWith(): any { buildWith(a: any, b: any, c: any): any {
return; return;
} }
buildJoin(): any { buildJoin(a: any, b: any): any {
return; return;
} }
} }
@@ -77,15 +75,4 @@ describe("[data] EntityRelation", async () => {
const relation2 = new TestEntityRelation({ required: true }); const relation2 = new TestEntityRelation({ required: true });
expect(relation2.required).toBe(true); expect(relation2.required).toBe(true);
}); });
it("correctly produces the relation name", async () => {
const relation = new ManyToManyRelation(new Entity("apps"), new Entity("organizations"));
expect(relation.getName()).not.toContain(",");
expect(relation.getName()).toBe("mn_apps_organizations");
const relation2 = new ManyToManyRelation(new Entity("apps"), new Entity("organizations"), {
connectionTableMappedName: "appOrganizations",
});
expect(relation2.getName()).toBe("mn_apps_organizations_appOrganizations");
});
}); });
@@ -1,24 +0,0 @@
import { describe, it, expect } from "bun:test";
import * as sDirect from "jsonv-ts";
import { s as sFromBknd } from "bknd/utils";
describe("jsonv-ts resolution", () => {
it("should resolve to a single instance", () => {
const sameNamespace = sDirect === (sFromBknd as unknown as typeof sDirect);
// If this fails, two instances are being loaded via different specifiers/paths
expect(sameNamespace).toBe(true);
});
it("should resolve specifiers to a single package path", async () => {
const base = await import.meta.resolve("jsonv-ts");
const hono = await import.meta.resolve("jsonv-ts/hono");
const mcp = await import.meta.resolve("jsonv-ts/mcp");
expect(typeof base).toBe("string");
expect(typeof hono).toBe("string");
expect(typeof mcp).toBe("string");
// They can be different files (subpath exports), but they should share the same package root
const pkgRoot = (p: string) => p.slice(0, p.lastIndexOf("jsonv-ts") + "jsonv-ts".length);
expect(pkgRoot(base)).toBe(pkgRoot(hono));
expect(pkgRoot(base)).toBe(pkgRoot(mcp));
});
});
+1 -1
View File
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { Flow, LogTask, SubFlowTask, RenderTask, Task } from "../../src/flows"; import { Flow, LogTask, SubFlowTask, RenderTask, Task } from "../../src/flows";
import { s } from "core/utils/schema"; import { s } from "core/object/schema";
export class StringifyTask<Output extends string> extends Task< export class StringifyTask<Output extends string> extends Task<
typeof StringifyTask.schema, typeof StringifyTask.schema,
+1 -1
View File
@@ -1,7 +1,7 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { Task } from "../../src/flows"; import { Task } from "../../src/flows";
import { dynamic } from "../../src/flows/tasks/Task"; import { dynamic } from "../../src/flows/tasks/Task";
import { s } from "core/utils/schema"; import { s } from "core/object/schema";
describe.skip("Task", async () => { describe.skip("Task", async () => {
test("resolveParams: template with parse", async () => { test("resolveParams: template with parse", async () => {
+1 -1
View File
@@ -1,7 +1,7 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { Hono } from "hono"; import { Hono } from "hono";
import { Event, EventManager } from "../../src/core/events"; import { Event, EventManager } from "../../src/core/events";
import { s, parse } from "core/utils/schema"; import { s, parse } from "core/object/schema";
import { EventTrigger, Flow, HttpTrigger, type InputsMap, Task } from "../../src/flows"; import { EventTrigger, Flow, HttpTrigger, type InputsMap, Task } from "../../src/flows";
import { dynamic } from "../../src/flows/tasks/Task"; import { dynamic } from "../../src/flows/tasks/Task";
+1 -1
View File
@@ -2,7 +2,7 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { isEqual } from "lodash-es"; import { isEqual } from "lodash-es";
import { _jsonp, withDisabledConsole } from "../../src/core/utils"; import { _jsonp, withDisabledConsole } from "../../src/core/utils";
import { s } from "core/utils/schema"; import { s } from "core/object/schema";
import { Condition, ExecutionEvent, FetchTask, Flow, LogTask, Task } from "../../src/flows"; import { Condition, ExecutionEvent, FetchTask, Flow, LogTask, Task } from "../../src/flows";
/*beforeAll(disableConsoleLog); /*beforeAll(disableConsoleLog);
+23 -4
View File
@@ -2,12 +2,11 @@ import { unlink } from "node:fs/promises";
import type { SelectQueryBuilder, SqliteDatabase } from "kysely"; import type { SelectQueryBuilder, SqliteDatabase } from "kysely";
import Database from "libsql"; import Database from "libsql";
import { format as sqlFormat } from "sql-formatter"; import { format as sqlFormat } from "sql-formatter";
import { type Connection, EntityManager, SqliteLocalConnection } from "../src/data";
import type { em as protoEm } from "../src/data/prototype"; import type { em as protoEm } from "../src/data/prototype";
import { writeFile } from "node:fs/promises"; import { writeFile } from "node:fs/promises";
import { join } from "node:path"; import { join } from "node:path";
import { slugify } from "bknd/utils"; import { slugify } from "core/utils/strings";
import { type Connection, SqliteLocalConnection } from "data/connection";
import { EntityManager } from "data/entities/EntityManager";
export function getDummyDatabase(memory: boolean = true): { export function getDummyDatabase(memory: boolean = true): {
dummyDb: SqliteDatabase; dummyDb: SqliteDatabase;
@@ -39,6 +38,26 @@ export function getLocalLibsqlConnection() {
return { url: "http://127.0.0.1:8080" }; return { url: "http://127.0.0.1:8080" };
} }
type ConsoleSeverity = "debug" | "log" | "warn" | "error";
const _oldConsoles = {
debug: console.debug,
log: console.log,
warn: console.warn,
error: console.error,
};
export function disableConsoleLog(severities: ConsoleSeverity[] = ["debug", "log", "warn"]) {
severities.forEach((severity) => {
console[severity] = () => null;
});
}
export function enableConsoleLog() {
Object.entries(_oldConsoles).forEach(([severity, fn]) => {
console[severity as ConsoleSeverity] = fn;
});
}
export function compileQb(qb: SelectQueryBuilder<any, any, any>) { export function compileQb(qb: SelectQueryBuilder<any, any, any>) {
const { sql, parameters } = qb.compile(); const { sql, parameters } = qb.compile();
return { sql, parameters }; return { sql, parameters };
@@ -46,7 +65,7 @@ export function compileQb(qb: SelectQueryBuilder<any, any, any>) {
export function prettyPrintQb(qb: SelectQueryBuilder<any, any, any>) { export function prettyPrintQb(qb: SelectQueryBuilder<any, any, any>) {
const { sql, parameters } = qb.compile(); const { sql, parameters } = qb.compile();
console.info("$", sqlFormat(sql), "\n[params]", parameters); console.log("$", sqlFormat(sql), "\n[params]", parameters);
} }
export function schemaToEm(s: ReturnType<typeof protoEm>, conn?: Connection): EntityManager<any> { export function schemaToEm(s: ReturnType<typeof protoEm>, conn?: Connection): EntityManager<any> {
@@ -1,9 +1,12 @@
import { afterAll, beforeAll, describe, expect, it } from "bun:test"; import { afterAll, afterEach, beforeAll, describe, expect, it } from "bun:test";
import { App, createApp, type AuthResponse } from "../../src"; import { App, createApp } from "../../src";
import type { AuthResponse } from "../../src/auth";
import { auth } from "../../src/auth/middlewares"; import { auth } from "../../src/auth/middlewares";
import { randomString, secureRandomString, withDisabledConsole } from "../../src/core/utils"; import { randomString, secureRandomString, withDisabledConsole } from "../../src/core/utils";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test"; import { disableConsoleLog, enableConsoleLog, getDummyConnection } from "../helper";
import { getDummyConnection } from "../helper";
const { dummyConnection, afterAllCleanup } = getDummyConnection();
afterEach(afterAllCleanup);
beforeAll(disableConsoleLog); beforeAll(disableConsoleLog);
afterAll(enableConsoleLog); afterAll(enableConsoleLog);
@@ -63,10 +66,9 @@ const configs = {
}; };
function createAuthApp() { function createAuthApp() {
const { dummyConnection } = getDummyConnection();
const app = createApp({ const app = createApp({
connection: dummyConnection, connection: dummyConnection,
config: { initialConfig: {
auth: configs.auth, auth: configs.auth,
}, },
}); });
@@ -149,8 +151,8 @@ describe("integration auth", () => {
const { data: users } = await app.em.repository("users").findMany(); const { data: users } = await app.em.repository("users").findMany();
expect(users.length).toBe(2); expect(users.length).toBe(2);
expect(users[0]?.email).toBe(configs.users.normal.email); expect(users[0].email).toBe(configs.users.normal.email);
expect(users[1]?.email).toBe(configs.users.admin.email); expect(users[1].email).toBe(configs.users.admin.email);
}); });
it("should log you in with API", async () => { it("should log you in with API", async () => {
@@ -221,7 +223,7 @@ describe("integration auth", () => {
app.server.get("/get", auth(), async (c) => { app.server.get("/get", auth(), async (c) => {
return c.json({ return c.json({
user: c.get("auth")?.user ?? null, user: c.get("auth").user ?? null,
}); });
}); });
app.server.get("/wait", auth(), async (c) => { app.server.get("/wait", auth(), async (c) => {
@@ -240,7 +242,7 @@ describe("integration auth", () => {
{ {
await new Promise((r) => setTimeout(r, 10)); await new Promise((r) => setTimeout(r, 10));
const res = await app.server.request("/get"); const res = await app.server.request("/get");
const data = (await res.json()) as any; const data = await res.json();
expect(data.user).toBe(null); expect(data.user).toBe(null);
expect(await $fns.me()).toEqual({ user: null as any }); expect(await $fns.me()).toEqual({ user: null as any });
} }
@@ -1,10 +1,6 @@
import { afterAll, beforeAll, describe, expect, it } from "bun:test"; import { describe, expect, it } from "bun:test";
import { createApp } from "core/test/utils"; import { createApp } from "core/test/utils";
import { Api } from "../../src/Api"; import { Api } from "../../src/Api";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
describe("integration config", () => { describe("integration config", () => {
it("should create an entity", async () => { it("should create an entity", async () => {
+8 -44
View File
@@ -1,25 +1,17 @@
/// <reference types="@types/bun" /> /// <reference types="@types/bun" />
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { registries } from "../../src";
import { createApp } from "core/test/utils"; import { createApp } from "core/test/utils";
import { mergeObject, randomString } from "../../src/core/utils"; import { mergeObject, randomString } from "../../src/core/utils";
import type { TAppMediaConfig } from "../../src/media/media-schema"; import type { TAppMediaConfig } from "../../src/media/media-schema";
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter"; import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
import { assetsPath, assetsTmpPath } from "../helper"; import { assetsPath, assetsTmpPath, disableConsoleLog, enableConsoleLog } from "../helper";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
beforeAll(() => {
//disableConsoleLog();
registries.media.register("local", StorageLocalAdapter);
});
afterAll(enableConsoleLog);
const path = `${assetsPath}/image.png`; const path = `${assetsPath}/image.png`;
async function makeApp(mediaOverride: Partial<TAppMediaConfig> = {}) { async function makeApp(mediaOverride: Partial<TAppMediaConfig> = {}) {
const app = createApp({ const app = createApp({
config: { initialConfig: {
media: mergeObject( media: mergeObject(
{ {
enabled: true, enabled: true,
@@ -35,6 +27,8 @@ async function makeApp(mediaOverride: Partial<TAppMediaConfig> = {}) {
}, },
}); });
app.module.media.adapters.set("local", StorageLocalAdapter);
await app.build(); await app.build();
return app; return app;
} }
@@ -43,6 +37,9 @@ function makeName(ext: string) {
return randomString(10) + "." + ext; return randomString(10) + "." + ext;
} }
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
describe("MediaController", () => { describe("MediaController", () => {
test("accepts direct", async () => { test("accepts direct", async () => {
const app = await makeApp(); const app = await makeApp();
@@ -54,6 +51,7 @@ describe("MediaController", () => {
body: file, body: file,
}); });
const result = (await res.json()) as any; const result = (await res.json()) as any;
console.log(result);
expect(result.name).toBe(name); expect(result.name).toBe(name);
const destFile = Bun.file(assetsTmpPath + "/" + name); const destFile = Bun.file(assetsTmpPath + "/" + name);
@@ -94,38 +92,4 @@ describe("MediaController", () => {
expect(res.status).toBe(413); expect(res.status).toBe(413);
expect(await Bun.file(assetsTmpPath + "/" + name).exists()).toBe(false); expect(await Bun.file(assetsTmpPath + "/" + name).exists()).toBe(false);
}); });
test("audio files", async () => {
const app = await makeApp();
const file = Bun.file(`${assetsPath}/test.mp3`);
const name = makeName("mp3");
const res = await app.server.request("/api/media/upload/" + name, {
method: "POST",
body: file,
});
const result = (await res.json()) as any;
expect(result.data.mime_type).toStartWith("audio/mpeg");
expect(result.name).toBe(name);
const destFile = Bun.file(assetsTmpPath + "/" + name);
expect(destFile.exists()).resolves.toBe(true);
await destFile.delete();
});
test("text files", async () => {
const app = await makeApp();
const file = Bun.file(`${assetsPath}/test.txt`);
const name = makeName("txt");
const res = await app.server.request("/api/media/upload/" + name, {
method: "POST",
body: file,
});
const result = (await res.json()) as any;
expect(result.data.mime_type).toStartWith("text/plain");
expect(result.name).toBe(name);
const destFile = Bun.file(assetsTmpPath + "/" + name);
expect(destFile.exists()).resolves.toBe(true);
await destFile.delete();
});
}); });
+1 -1
View File
@@ -1,7 +1,7 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { type FileBody, Storage } from "../../src/media/storage/Storage"; import { type FileBody, Storage } from "../../src/media/storage/Storage";
import * as StorageEvents from "../../src/media/storage/events"; import * as StorageEvents from "../../src/media/storage/events";
import { StorageAdapter } from "media/storage/StorageAdapter"; import { StorageAdapter } from "media";
class TestAdapter extends StorageAdapter { class TestAdapter extends StorageAdapter {
files: Record<string, FileBody> = {}; files: Record<string, FileBody> = {};
-37
View File
@@ -71,8 +71,6 @@ describe("media/mime-types", () => {
["application/zip", "zip"], ["application/zip", "zip"],
["text/tab-separated-values", "tsv"], ["text/tab-separated-values", "tsv"],
["application/zip", "zip"], ["application/zip", "zip"],
["application/pdf", "pdf"],
["audio/mpeg", "mp3"],
] as const; ] as const;
for (const [mime, ext] of tests) { for (const [mime, ext] of tests) {
@@ -90,9 +88,6 @@ describe("media/mime-types", () => {
["image.jpeg", "jpeg"], ["image.jpeg", "jpeg"],
["-473Wx593H-466453554-black-MODEL.jpg", "jpg"], ["-473Wx593H-466453554-black-MODEL.jpg", "jpg"],
["-473Wx593H-466453554-black-MODEL.avif", "avif"], ["-473Wx593H-466453554-black-MODEL.avif", "avif"],
["file.pdf", "pdf"],
["file.mp3", "mp3"],
["robots.txt", "txt"],
] as const; ] as const;
for (const [filename, ext] of tests) { for (const [filename, ext] of tests) {
@@ -107,36 +102,4 @@ describe("media/mime-types", () => {
const [, ext] = getRandomizedFilename(file).split("."); const [, ext] = getRandomizedFilename(file).split(".");
expect(ext).toBe("jpg"); expect(ext).toBe("jpg");
}); });
test("getRandomizedFilename with body", async () => {
// should keep "pdf"
const [, ext] = getRandomizedFilename(
new File([""], "file.pdf", { type: "application/pdf" }),
).split(".");
expect(ext).toBe("pdf");
{
// no ext, should use "pdf" only for known formats
const [, ext] = getRandomizedFilename(
new File([""], "file", { type: "application/pdf" }),
).split(".");
expect(ext).toBe("pdf");
}
{
// wrong ext, should keep the wrong one
const [, ext] = getRandomizedFilename(
new File([""], "file.what", { type: "application/pdf" }),
).split(".");
expect(ext).toBe("what");
}
{
// txt
const [, ext] = getRandomizedFilename(
new File([""], "file.txt", { type: "text/plain" }),
).split(".");
expect(ext).toBe("txt");
}
});
}); });
+8 -10
View File
@@ -1,16 +1,14 @@
import { afterAll, beforeAll, beforeEach, describe, expect, spyOn, test } from "bun:test"; import { afterAll, beforeAll, beforeEach, describe, expect, spyOn, test } from "bun:test";
import { createApp } from "core/test/utils"; import { createApp } from "core/test/utils";
import { AuthController } from "../../src/auth/api/AuthController"; import { AuthController } from "../../src/auth/api/AuthController";
import { em, entity, make, text } from "data/prototype"; import { em, entity, make, text } from "../../src/data";
import { AppAuth, type ModuleBuildContext } from "modules"; import { AppAuth, type ModuleBuildContext } from "../../src/modules";
import { disableConsoleLog, enableConsoleLog } from "../helper";
// @ts-ignore
import { makeCtx, moduleTestSuite } from "./module-test-suite"; import { makeCtx, moduleTestSuite } from "./module-test-suite";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
describe("AppAuth", () => { describe("AppAuth", () => {
test.skip("...", () => { test.only("...", () => {
const auth = new AppAuth({}); const auth = new AppAuth({});
console.log(auth.toJSON()); console.log(auth.toJSON());
console.log(auth.config); console.log(auth.config);
@@ -150,7 +148,7 @@ describe("AppAuth", () => {
test("registers auth middleware for bknd routes only", async () => { test("registers auth middleware for bknd routes only", async () => {
const app = createApp({ const app = createApp({
config: { initialConfig: {
auth: { auth: {
enabled: true, enabled: true,
jwt: { jwt: {
@@ -180,7 +178,7 @@ describe("AppAuth", () => {
test("should allow additional user fields", async () => { test("should allow additional user fields", async () => {
const app = createApp({ const app = createApp({
config: { initialConfig: {
auth: { auth: {
entity_name: "users", entity_name: "users",
enabled: true, enabled: true,
@@ -204,7 +202,7 @@ describe("AppAuth", () => {
test("ensure user field configs is always correct", async () => { test("ensure user field configs is always correct", async () => {
const app = createApp({ const app = createApp({
config: { initialConfig: {
auth: { auth: {
enabled: true, enabled: true,
}, },
+1 -1
View File
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, test } from "bun:test"; import { beforeEach, describe, expect, test } from "bun:test";
import { parse } from "core/utils/schema"; import { parse } from "core/object/schema";
import { fieldsSchema } from "../../src/data/data-schema"; import { fieldsSchema } from "../../src/data/data-schema";
import { AppData, type ModuleBuildContext } from "../../src/modules"; import { AppData, type ModuleBuildContext } from "../../src/modules";
import { makeCtx, moduleTestSuite } from "./module-test-suite"; import { makeCtx, moduleTestSuite } from "./module-test-suite";
+4 -9
View File
@@ -1,24 +1,18 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { createApp } from "core/test/utils"; import { createApp } from "core/test/utils";
import { em, entity, text } from "data/prototype"; import { em, entity, text } from "../../src/data";
import { registries } from "modules/registries";
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter"; import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
import { AppMedia } from "../../src/media/AppMedia"; import { AppMedia } from "../../src/media/AppMedia";
import { moduleTestSuite } from "./module-test-suite"; import { moduleTestSuite } from "./module-test-suite";
describe("AppMedia", () => { describe("AppMedia", () => {
test.skip("...", () => {
const media = new AppMedia();
console.log(media.toJSON());
});
moduleTestSuite(AppMedia); moduleTestSuite(AppMedia);
test("should allow additional fields", async () => { test("should allow additional fields", async () => {
registries.media.register("local", StorageLocalAdapter); //registries.media.register("local", StorageLocalAdapter);
const app = createApp({ const app = createApp({
config: { initialConfig: {
media: { media: {
entity_name: "media", entity_name: "media",
enabled: true, enabled: true,
@@ -36,6 +30,7 @@ describe("AppMedia", () => {
}).toJSON(), }).toJSON(),
}, },
}); });
app.module.media.adapters.set("local", StorageLocalAdapter);
await app.build(); await app.build();
@@ -1,76 +0,0 @@
import { it, expect, describe } from "bun:test";
import { DbModuleManager } from "modules/db/DbModuleManager";
import { getDummyConnection } from "../helper";
import { TABLE_NAME } from "modules/db/migrations";
describe("DbModuleManager", () => {
it("should extract secrets", async () => {
const { dummyConnection } = getDummyConnection();
const m = new DbModuleManager(dummyConnection, {
initial: {
auth: {
enabled: true,
jwt: {
secret: "test",
},
},
},
});
await m.build();
expect(m.toJSON(true).auth.jwt.secret).toBe("test");
await m.save();
});
it("should work with initial secrets", async () => {
const { dummyConnection } = getDummyConnection();
const db = dummyConnection.kysely;
const m = new DbModuleManager(dummyConnection, {
initial: {
auth: {
enabled: true,
jwt: {
secret: "",
},
},
},
secrets: {
"auth.jwt.secret": "test",
},
});
await m.build();
expect(m.toJSON(true).auth.jwt.secret).toBe("test");
const getSecrets = () =>
db
.selectFrom(TABLE_NAME)
.selectAll()
.where("type", "=", "secrets")
.executeTakeFirst()
.then((r) => r?.json);
expect(await getSecrets()).toEqual({ "auth.jwt.secret": "test" });
// also after rebuild
await m.build();
await m.save();
expect(await getSecrets()).toEqual({ "auth.jwt.secret": "test" });
// and ignore if already present
const m2 = new DbModuleManager(dummyConnection, {
initial: {
auth: {
enabled: true,
jwt: {
secret: "",
},
},
},
secrets: {
"auth.jwt.secret": "something completely different",
},
});
await m2.build();
await m2.save();
expect(await getSecrets()).toEqual({ "auth.jwt.secret": "test" });
});
});
+2 -3
View File
@@ -1,7 +1,6 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { s, stripMark } from "core/utils/schema"; import { s, stripMark } from "core/object/schema";
import { em, entity, index, text } from "data/prototype"; import { EntityManager, em, entity, index, text } from "../../src/data";
import { EntityManager } from "data/entities/EntityManager";
import { DummyConnection } from "../../src/data/connection/DummyConnection"; import { DummyConnection } from "../../src/data/connection/DummyConnection";
import { Module } from "../../src/modules/Module"; import { Module } from "../../src/modules/Module";
import { ModuleHelper } from "modules/ModuleHelper"; import { ModuleHelper } from "modules/ModuleHelper";
+7 -14
View File
@@ -1,18 +1,11 @@
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, mock, test } from "bun:test"; import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test"; import { disableConsoleLog, enableConsoleLog } from "core/utils";
import { Connection, entity, text } from "data";
import { Module } from "modules/Module"; import { Module } from "modules/Module";
import { getDefaultConfig } from "modules/ModuleManager"; import { type ConfigTable, getDefaultConfig, ModuleManager } from "modules/ModuleManager";
import { type ConfigTable, DbModuleManager as ModuleManager } from "modules/db/DbModuleManager"; import { CURRENT_VERSION, TABLE_NAME } from "modules/migrations";
import { CURRENT_VERSION, TABLE_NAME } from "modules/db/migrations";
import { getDummyConnection } from "../helper"; import { getDummyConnection } from "../helper";
import { s, stripMark } from "core/utils/schema"; import { s, stripMark } from "core/object/schema";
import { Connection } from "data/connection/Connection";
import { entity, text } from "data/prototype";
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
describe("ModuleManager", async () => { describe("ModuleManager", async () => {
test("s1: no config, no build", async () => { test("s1: no config, no build", async () => {
@@ -138,7 +131,7 @@ describe("ModuleManager", async () => {
const db = c2.dummyConnection.kysely; const db = c2.dummyConnection.kysely;
const mm2 = new ModuleManager(c2.dummyConnection, { const mm2 = new ModuleManager(c2.dummyConnection, {
initial: { version: version - 1, ...json } as any, initial: { version: version - 1, ...json },
}); });
await mm2.syncConfigTable(); await mm2.syncConfigTable();
await db await db
@@ -1,22 +1,14 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { App, type InitialModuleConfigs, createApp } from "/"; import { type InitialModuleConfigs, createApp } from "../../../src";
import { type Kysely, sql } from "kysely"; import { type Kysely, sql } from "kysely";
import { getDummyConnection } from "../../helper"; import { getDummyConnection } from "../../helper";
import v7 from "./samples/v7.json"; 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 { disableConsoleLog, enableConsoleLog } from "core/utils/test";
beforeAll(() => disableConsoleLog());
afterAll(enableConsoleLog);
// app expects migratable config to be present in database // app expects migratable config to be present in database
async function createVersionedApp( async function createVersionedApp(config: InitialModuleConfigs | any) {
config: InitialModuleConfigs | any,
opts?: { beforeCreateApp?: (db: Kysely<any>) => Promise<void> },
) {
const { dummyConnection } = getDummyConnection(); const { dummyConnection } = getDummyConnection();
if (!("version" in config)) throw new Error("config must have a version"); if (!("version" in config)) throw new Error("config must have a version");
@@ -42,10 +34,6 @@ async function createVersionedApp(
}) })
.execute(); .execute();
if (opts?.beforeCreateApp) {
await opts.beforeCreateApp(db);
}
const app = createApp({ const app = createApp({
connection: dummyConnection, connection: dummyConnection,
}); });
@@ -53,19 +41,6 @@ async function createVersionedApp(
return app; return app;
} }
async function getRawConfig(
app: App,
opts?: { version?: number; types?: ("config" | "diff" | "backup" | "secrets")[] },
) {
const db = app.em.connection.kysely;
return await db
.selectFrom("__bknd")
.selectAll()
.$if(!!opts?.version, (qb) => qb.where("version", "=", opts?.version))
.$if((opts?.types?.length ?? 0) > 0, (qb) => qb.where("type", "in", opts?.types))
.execute();
}
describe("Migrations", () => { describe("Migrations", () => {
/** /**
* updated auth strategies to have "enabled" prop * updated auth strategies to have "enabled" prop
@@ -103,30 +78,4 @@ describe("Migrations", () => {
// @ts-expect-error // @ts-expect-error
expect(app.toJSON(true).server.admin).toBeUndefined(); expect(app.toJSON(true).server.admin).toBeUndefined();
}); });
test("migration from 9 to 10", async () => {
expect(v9.version).toBe(9);
const app = await createVersionedApp(v9);
expect(app.version()).toBeGreaterThan(9);
// @ts-expect-error
expect(app.toJSON(true).media.adapter.config.secret_access_key).toBe(
"^^s3.secret_access_key^^",
);
const [config, secrets] = (await getRawConfig(app, {
version: 10,
types: ["config", "secrets"],
})) as any;
expect(config.json.auth.jwt.secret).toBe("");
expect(config.json.media.adapter.config.access_key).toBe("");
expect(config.json.media.adapter.config.secret_access_key).toBe("");
expect(secrets.json["auth.jwt.secret"]).toBe("^^jwt.secret^^");
expect(secrets.json["media.adapter.config.access_key"]).toBe("^^s3.access_key^^");
expect(secrets.json["media.adapter.config.secret_access_key"]).toBe(
"^^s3.secret_access_key^^",
);
});
}); });
@@ -1,612 +0,0 @@
{
"version": 9,
"server": {
"cors": {
"origin": "*",
"allow_methods": ["GET", "POST", "PATCH", "PUT", "DELETE"],
"allow_headers": [
"Content-Type",
"Content-Length",
"Authorization",
"Accept"
],
"allow_credentials": true
},
"mcp": { "enabled": false, "path": "/api/system/mcp" }
},
"data": {
"basepath": "/api/data",
"default_primary_format": "integer",
"entities": {
"media": {
"type": "system",
"fields": {
"id": {
"type": "primary",
"config": {
"format": "integer",
"fillable": false,
"required": false
}
},
"path": { "type": "text", "config": { "required": true } },
"folder": {
"type": "boolean",
"config": {
"default_value": false,
"hidden": true,
"fillable": ["create"],
"required": false
}
},
"mime_type": { "type": "text", "config": { "required": false } },
"size": { "type": "number", "config": { "required": false } },
"scope": {
"type": "text",
"config": {
"hidden": true,
"fillable": ["create"],
"required": false
}
},
"etag": { "type": "text", "config": { "required": false } },
"modified_at": {
"type": "date",
"config": { "type": "datetime", "required": false }
},
"reference": { "type": "text", "config": { "required": false } },
"entity_id": { "type": "number", "config": { "required": false } },
"metadata": { "type": "json", "config": { "required": false } }
},
"config": { "sort_field": "id", "sort_dir": "asc" }
},
"users": {
"type": "system",
"fields": {
"id": {
"type": "primary",
"config": {
"format": "integer",
"fillable": false,
"required": false
}
},
"email": { "type": "text", "config": { "required": true } },
"strategy": {
"type": "enum",
"config": {
"options": { "type": "strings", "values": ["password"] },
"required": true,
"hidden": ["update", "form"],
"fillable": ["create"]
}
},
"strategy_value": {
"type": "text",
"config": {
"fillable": ["create"],
"hidden": ["read", "table", "update", "form"],
"required": true
}
},
"role": {
"type": "enum",
"config": {
"options": { "type": "strings", "values": ["admin", "guest"] },
"required": false
}
},
"age": {
"type": "enum",
"config": {
"options": {
"type": "strings",
"values": ["18-24", "25-34", "35-44", "45-64", "65+"]
},
"required": false
}
},
"height": { "type": "number", "config": { "required": false } },
"gender": {
"type": "enum",
"config": {
"options": { "type": "strings", "values": ["male", "female"] },
"required": false
}
}
},
"config": { "sort_field": "id", "sort_dir": "asc" }
},
"avatars": {
"type": "regular",
"fields": {
"id": {
"type": "primary",
"config": {
"format": "integer",
"fillable": false,
"required": false
}
},
"identifier": { "type": "text", "config": { "required": false } },
"payload": {
"type": "json",
"config": { "required": false, "hidden": ["table"] }
},
"created_at": {
"type": "date",
"config": { "type": "datetime", "required": false }
},
"started_at": {
"type": "date",
"config": { "type": "datetime", "required": false }
},
"completed_at": {
"type": "date",
"config": { "type": "datetime", "required": false }
},
"input": {
"type": "media",
"config": {
"required": false,
"fillable": ["update"],
"hidden": false,
"mime_types": [],
"virtual": true,
"entity": "avatars"
}
},
"output": {
"type": "media",
"config": {
"required": false,
"fillable": ["update"],
"hidden": false,
"mime_types": [],
"virtual": true,
"entity": "avatars"
}
},
"users_id": {
"type": "relation",
"config": {
"label": "Users",
"required": false,
"reference": "users",
"target": "users",
"target_field": "id",
"target_field_type": "integer",
"on_delete": "set null"
}
}
},
"config": { "sort_field": "id", "sort_dir": "desc" }
},
"tryons": {
"type": "regular",
"fields": {
"id": {
"type": "primary",
"config": {
"format": "integer",
"fillable": false,
"required": false
}
},
"created_at": {
"type": "date",
"config": { "type": "datetime", "required": false }
},
"completed_at": {
"type": "date",
"config": { "type": "datetime", "required": false }
},
"avatars_id": {
"type": "relation",
"config": {
"label": "Avatars",
"required": false,
"reference": "avatars",
"target": "avatars",
"target_field": "id",
"target_field_type": "integer",
"on_delete": "set null"
}
},
"users_id": {
"type": "relation",
"config": {
"label": "Users",
"required": false,
"reference": "users",
"target": "users",
"target_field": "id",
"target_field_type": "integer",
"on_delete": "set null"
}
},
"output": {
"type": "media",
"config": {
"required": false,
"fillable": ["update"],
"hidden": false,
"mime_types": [],
"virtual": true,
"entity": "tryons",
"max_items": 1
}
},
"products_id": {
"type": "relation",
"config": {
"label": "Products",
"required": false,
"reference": "products",
"target": "products",
"target_field": "id",
"target_field_type": "integer",
"on_delete": "set null"
}
},
"payload": {
"type": "json",
"config": { "required": false, "hidden": ["table"] }
}
},
"config": { "sort_field": "id", "sort_dir": "desc" }
},
"products": {
"type": "regular",
"fields": {
"id": {
"type": "primary",
"config": {
"format": "integer",
"fillable": false,
"required": false
}
},
"enabled": { "type": "boolean", "config": { "required": false } },
"title": { "type": "text", "config": { "required": false } },
"url": { "type": "text", "config": { "required": false } },
"image": {
"type": "media",
"config": {
"required": false,
"fillable": ["update"],
"hidden": false,
"mime_types": [],
"virtual": true,
"entity": "products",
"max_items": 1
}
},
"created_at": {
"type": "date",
"config": { "type": "datetime", "required": false }
},
"sites_id": {
"type": "relation",
"config": {
"label": "Sites",
"required": false,
"reference": "sites",
"target": "sites",
"target_field": "id",
"target_field_type": "integer",
"on_delete": "set null"
}
},
"garment_type": {
"type": "enum",
"config": {
"options": {
"type": "strings",
"values": ["auto", "tops", "bottoms", "one-pieces"]
},
"required": false
}
}
},
"config": { "sort_field": "id", "sort_dir": "desc" }
},
"sites": {
"type": "regular",
"fields": {
"id": {
"type": "primary",
"config": {
"format": "integer",
"fillable": false,
"required": false
}
},
"origin": {
"type": "text",
"config": {
"pattern": "^(https?):\\/\\/([a-zA-Z0-9.-]+)(:\\d+)?$",
"required": true
}
},
"name": { "type": "text", "config": { "required": false } },
"active": { "type": "boolean", "config": { "required": false } },
"logo": {
"type": "media",
"config": {
"required": false,
"fillable": ["update"],
"hidden": false,
"mime_types": [],
"virtual": true,
"entity": "sites",
"max_items": 1
}
},
"instructions": {
"type": "text",
"config": {
"html_config": {
"element": "textarea",
"props": { "rows": "2" }
},
"required": false,
"hidden": ["table"]
}
}
},
"config": { "sort_field": "id", "sort_dir": "desc" }
},
"sessions": {
"type": "regular",
"fields": {
"id": {
"type": "primary",
"config": { "format": "uuid", "fillable": false, "required": false }
},
"created_at": {
"type": "date",
"config": { "type": "datetime", "required": true }
},
"claimed_at": {
"type": "date",
"config": { "type": "datetime", "required": false }
},
"url": { "type": "text", "config": { "required": false } },
"sites_id": {
"type": "relation",
"config": {
"label": "Sites",
"required": false,
"reference": "sites",
"target": "sites",
"target_field": "id",
"target_field_type": "integer",
"on_delete": "set null"
}
},
"users_id": {
"type": "relation",
"config": {
"label": "Users",
"required": false,
"reference": "users",
"target": "users",
"target_field": "id",
"target_field_type": "integer",
"on_delete": "set null"
}
}
},
"config": { "sort_field": "id", "sort_dir": "desc" }
}
},
"relations": {
"poly_avatars_media_input": {
"type": "poly",
"source": "avatars",
"target": "media",
"config": { "mappedBy": "input" }
},
"poly_avatars_media_output": {
"type": "poly",
"source": "avatars",
"target": "media",
"config": { "mappedBy": "output" }
},
"n1_avatars_users": {
"type": "n:1",
"source": "avatars",
"target": "users",
"config": {
"mappedBy": "",
"inversedBy": "",
"required": false,
"with_limit": 5
}
},
"n1_tryons_avatars": {
"type": "n:1",
"source": "tryons",
"target": "avatars",
"config": {
"mappedBy": "",
"inversedBy": "",
"required": false,
"with_limit": 5
}
},
"n1_tryons_users": {
"type": "n:1",
"source": "tryons",
"target": "users",
"config": {
"mappedBy": "",
"inversedBy": "",
"required": false,
"with_limit": 5
}
},
"poly_tryons_media_output": {
"type": "poly",
"source": "tryons",
"target": "media",
"config": { "mappedBy": "output", "targetCardinality": 1 }
},
"poly_products_media_image": {
"type": "poly",
"source": "products",
"target": "media",
"config": { "mappedBy": "image", "targetCardinality": 1 }
},
"n1_tryons_products": {
"type": "n:1",
"source": "tryons",
"target": "products",
"config": {
"mappedBy": "",
"inversedBy": "",
"required": false,
"with_limit": 5
}
},
"poly_sites_media_logo": {
"type": "poly",
"source": "sites",
"target": "media",
"config": { "mappedBy": "logo", "targetCardinality": 1 }
},
"n1_sessions_sites": {
"type": "n:1",
"source": "sessions",
"target": "sites",
"config": {
"mappedBy": "",
"inversedBy": "",
"required": false,
"with_limit": 5
}
},
"n1_sessions_users": {
"type": "n:1",
"source": "sessions",
"target": "users",
"config": {
"mappedBy": "",
"inversedBy": "",
"required": false,
"with_limit": 5
}
},
"n1_products_sites": {
"type": "n:1",
"source": "products",
"target": "sites",
"config": {
"mappedBy": "",
"inversedBy": "",
"required": false,
"with_limit": 5
}
}
},
"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
},
"idx_unique_users_email": {
"entity": "users",
"fields": ["email"],
"unique": true
},
"idx_users_strategy": {
"entity": "users",
"fields": ["strategy"],
"unique": false
},
"idx_users_strategy_value": {
"entity": "users",
"fields": ["strategy_value"],
"unique": false
},
"idx_sites_origin_active": {
"entity": "sites",
"fields": ["origin", "active"],
"unique": false
},
"idx_sites_active": {
"entity": "sites",
"fields": ["active"],
"unique": false
},
"idx_products_url": {
"entity": "products",
"fields": ["url"],
"unique": false
}
}
},
"auth": {
"enabled": true,
"basepath": "/api/auth",
"entity_name": "users",
"allow_register": true,
"jwt": {
"secret": "^^jwt.secret^^",
"alg": "HS256",
"expires": 999999999,
"issuer": "issuer",
"fields": ["id", "email", "role"]
},
"cookie": {
"path": "/",
"sameSite": "none",
"secure": true,
"httpOnly": true,
"expires": 604800,
"partitioned": false,
"renew": true,
"pathSuccess": "/admin",
"pathLoggedOut": "/"
},
"strategies": {
"password": {
"enabled": true,
"type": "password",
"config": { "hashing": "sha256" }
}
},
"guard": { "enabled": false },
"roles": {
"admin": { "implicit_allow": true },
"guest": { "is_default": true }
}
},
"media": {
"enabled": true,
"basepath": "/api/media",
"entity_name": "media",
"storage": { "body_max_size": 0 },
"adapter": {
"type": "s3",
"config": {
"access_key": "^^s3.access_key^^",
"secret_access_key": "^^s3.secret_access_key^^",
"url": "https://1234.r2.cloudflarestorage.com/bucket-name"
}
}
},
"flows": { "basepath": "/api/flows", "flows": {} }
}
+5 -6
View File
@@ -1,13 +1,13 @@
import { beforeEach, describe, expect, it } from "bun:test"; import { beforeEach, describe, expect, it } from "bun:test";
import { Hono } from "hono"; import { Hono } from "hono";
import { Guard } from "auth/authorize/Guard"; import { Guard } from "../../src/auth";
import { EventManager } from "core/events"; import { DebugLogger } from "../../src/core";
import { EntityManager } from "data/entities/EntityManager"; import { EventManager } from "../../src/core/events";
import { Module, type ModuleBuildContext } from "modules/Module"; import { EntityManager } from "../../src/data";
import { Module, type ModuleBuildContext } from "../../src/modules/Module";
import { getDummyConnection } from "../helper"; import { getDummyConnection } from "../helper";
import { ModuleHelper } from "modules/ModuleHelper"; import { ModuleHelper } from "modules/ModuleHelper";
import { DebugLogger, McpServer } from "bknd/utils";
export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildContext { export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildContext {
const { dummyConnection } = getDummyConnection(); const { dummyConnection } = getDummyConnection();
@@ -19,7 +19,6 @@ export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildCon
guard: new Guard(), guard: new Guard(),
flags: Module.ctx_flags, flags: Module.ctx_flags,
logger: new DebugLogger(false), logger: new DebugLogger(false),
mcp: new McpServer(),
...overrides, ...overrides,
}; };
return { return {
+1 -3
View File
@@ -102,9 +102,7 @@ describe("json form", () => {
] satisfies [string, Exclude<JSONSchema, boolean>, boolean][]; ] satisfies [string, Exclude<JSONSchema, boolean>, boolean][];
for (const [pointer, schema, output] of examples) { for (const [pointer, schema, output] of examples) {
expect(utils.isRequired(new Draft2019(schema), pointer, schema), `${pointer} `).toBe( expect(utils.isRequired(new Draft2019(schema), pointer, schema)).toBe(output);
output,
);
} }
}); });
+1 -25
View File
@@ -1,29 +1,6 @@
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 "core/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({ const result = await Bun.build({
entrypoints: ["./src/cli/index.ts"], entrypoints: ["./src/cli/index.ts"],
@@ -31,7 +8,6 @@ const result = await Bun.build({
outdir: "./dist/cli", outdir: "./dist/cli",
env: "PUBLIC_*", env: "PUBLIC_*",
minify: true, minify: true,
external,
define: { define: {
__isDev: "0", __isDev: "0",
__version: JSON.stringify(pkg.version), __version: JSON.stringify(pkg.version),
+82 -98
View File
@@ -1,7 +1,6 @@
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" };
import c from "picocolors";
const args = process.argv.slice(2); const args = process.argv.slice(2);
const watch = args.includes("--watch"); const watch = args.includes("--watch");
@@ -10,14 +9,6 @@ const types = args.includes("--types");
const sourcemap = args.includes("--sourcemap"); const sourcemap = args.includes("--sourcemap");
const clean = args.includes("--clean"); const clean = args.includes("--clean");
// silence tsup
const oldConsole = {
log: console.log,
warn: console.warn,
};
console.log = () => {};
console.warn = () => {};
const define = { const define = {
__isDev: "0", __isDev: "0",
__version: JSON.stringify(pkg.version), __version: JSON.stringify(pkg.version),
@@ -36,11 +27,11 @@ function buildTypes() {
Bun.spawn(["bun", "build:types"], { Bun.spawn(["bun", "build:types"], {
stdout: "inherit", stdout: "inherit",
onExit: () => { onExit: () => {
oldConsole.log(c.cyan("[Types]"), c.green("built")); console.info("Types built");
Bun.spawn(["bun", "tsc-alias"], { Bun.spawn(["bun", "tsc-alias"], {
stdout: "inherit", stdout: "inherit",
onExit: () => { onExit: () => {
oldConsole.log(c.cyan("[Types]"), c.green("aliased")); console.info("Types aliased");
types_running = false; types_running = false;
}, },
}); });
@@ -48,10 +39,6 @@ function buildTypes() {
}); });
} }
if (types && !watch) {
buildTypes();
}
let watcher_timeout: any; let watcher_timeout: any;
function delayTypes() { function delayTypes() {
if (!watch || !types) return; if (!watch || !types) return;
@@ -61,44 +48,57 @@ function delayTypes() {
watcher_timeout = setTimeout(buildTypes, 1000); watcher_timeout = setTimeout(buildTypes, 1000);
} }
const dependencies = Object.keys(pkg.dependencies); if (types && !watch) {
buildTypes();
}
function banner(title: string) {
console.info("");
console.info("=".repeat(40));
console.info(title.toUpperCase());
console.info("-".repeat(40));
}
// collection of always-external packages // collection of always-external packages
const external = [ const external = [
...dependencies,
"bun:test", "bun:test",
"node:test", "node:test",
"node:assert/strict", "node:assert/strict",
"@libsql/client", "@libsql/client",
"bknd", "bknd",
/^bknd\/.*/, /^bknd\/.*/,
"jsonv-ts",
/^jsonv-ts\/.*/,
] as const; ] as const;
/** /**
* Building backend and general API * Building backend and general API
*/ */
async function buildApi() { async function buildApi() {
banner("Building API");
await tsup.build({ await tsup.build({
minify, minify,
sourcemap, sourcemap,
watch, watch,
define, define,
entry: ["src/index.ts", "src/core/utils/index.ts", "src/plugins/index.ts"], entry: [
"src/index.ts",
"src/core/index.ts",
"src/core/utils/index.ts",
"src/data/index.ts",
"src/media/index.ts",
"src/plugins/index.ts",
],
outDir: "dist", outDir: "dist",
external: [...external], external: [...external],
metafile: true, metafile: true,
target: "esnext",
platform: "browser", platform: "browser",
format: ["esm"], format: ["esm"],
splitting: false, splitting: false,
treeshake: true,
loader: { loader: {
".svg": "dataurl", ".svg": "dataurl",
}, },
onSuccess: async () => { onSuccess: async () => {
delayTypes(); delayTypes();
oldConsole.log(c.cyan("[API]"), c.green("built"));
}, },
}); });
} }
@@ -142,6 +142,7 @@ async function buildUi() {
}, },
} satisfies tsup.Options; } satisfies tsup.Options;
banner("Building UI");
await tsup.build({ await tsup.build({
...base, ...base,
entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"], entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"],
@@ -149,10 +150,10 @@ async function buildUi() {
onSuccess: async () => { onSuccess: async () => {
await rewriteClient("./dist/ui/index.js"); await rewriteClient("./dist/ui/index.js");
delayTypes(); delayTypes();
oldConsole.log(c.cyan("[UI]"), c.green("built"));
}, },
}); });
banner("Building Client");
await tsup.build({ await tsup.build({
...base, ...base,
entry: ["src/ui/client/index.ts"], entry: ["src/ui/client/index.ts"],
@@ -160,7 +161,6 @@ async function buildUi() {
onSuccess: async () => { onSuccess: async () => {
await rewriteClient("./dist/ui/client/index.js"); await rewriteClient("./dist/ui/client/index.js");
delayTypes(); delayTypes();
oldConsole.log(c.cyan("[UI]"), "Client", c.green("built"));
}, },
}); });
} }
@@ -171,6 +171,7 @@ async function buildUi() {
* - ui/client is external, and after built replaced with "bknd/client" * - ui/client is external, and after built replaced with "bknd/client"
*/ */
async function buildUiElements() { async function buildUiElements() {
banner("Building UI Elements");
await tsup.build({ await tsup.build({
minify, minify,
sourcemap, sourcemap,
@@ -204,7 +205,6 @@ async function buildUiElements() {
onSuccess: async () => { onSuccess: async () => {
await rewriteClient("./dist/ui/elements/index.js"); await rewriteClient("./dist/ui/elements/index.js");
delayTypes(); delayTypes();
oldConsole.log(c.cyan("[UI]"), "Elements", c.green("built"));
}, },
}); });
} }
@@ -225,7 +225,6 @@ function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsu
splitting: false, splitting: false,
onSuccess: async () => { onSuccess: async () => {
delayTypes(); delayTypes();
oldConsole.log(c.cyan("[Adapter]"), adapter || "base", c.green("built"));
}, },
...overrides, ...overrides,
define: { define: {
@@ -244,80 +243,65 @@ function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsu
} }
async function buildAdapters() { async function buildAdapters() {
await Promise.all([ banner("Building Adapters");
// base adapter handles // base adapter handles
tsup.build({ await tsup.build({
...baseConfig(""), ...baseConfig(""),
target: "esnext", entry: ["src/adapter/index.ts"],
platform: "neutral", outDir: "dist/adapter",
entry: ["src/adapter/index.ts"], });
outDir: "dist/adapter",
// only way to keep @vite-ignore comments
minify: false,
}),
// specific adatpers // specific adatpers
tsup.build(baseConfig("react-router")), await tsup.build(baseConfig("react-router"));
tsup.build( await tsup.build(
baseConfig("bun", { baseConfig("bun", {
external: [/^bun\:.*/],
}),
),
tsup.build(baseConfig("astro")),
tsup.build(baseConfig("aws")),
tsup.build(
baseConfig("cloudflare", {
external: ["wrangler", "node:process"],
}),
),
tsup.build(
baseConfig("cloudflare/proxy", {
target: "esnext",
entry: ["src/adapter/cloudflare/proxy.ts"],
outDir: "dist/adapter/cloudflare",
metafile: false,
external: [/bknd/, "wrangler", "node:process"],
}),
),
tsup.build({
...baseConfig("vite"),
platform: "node",
}),
tsup.build({
...baseConfig("nextjs"),
platform: "node",
}),
tsup.build({
...baseConfig("node"),
platform: "node",
}),
tsup.build({
...baseConfig("sqlite/edge"),
entry: ["src/adapter/sqlite/edge.ts"],
outDir: "dist/adapter/sqlite",
metafile: false,
}),
tsup.build({
...baseConfig("sqlite/node"),
entry: ["src/adapter/sqlite/node.ts"],
outDir: "dist/adapter/sqlite",
platform: "node",
metafile: false,
}),
tsup.build({
...baseConfig("sqlite/bun"),
entry: ["src/adapter/sqlite/bun.ts"],
outDir: "dist/adapter/sqlite",
metafile: false,
external: [/^bun\:.*/], external: [/^bun\:.*/],
}), }),
]); );
await tsup.build(baseConfig("astro"));
await tsup.build(baseConfig("aws"));
await tsup.build(baseConfig("cloudflare"));
await tsup.build({
...baseConfig("vite"),
platform: "node",
});
await tsup.build({
...baseConfig("nextjs"),
platform: "node",
});
await tsup.build({
...baseConfig("node"),
platform: "node",
});
await tsup.build({
...baseConfig("sqlite/edge"),
entry: ["src/adapter/sqlite/edge.ts"],
outDir: "dist/adapter/sqlite",
metafile: false,
});
await tsup.build({
...baseConfig("sqlite/node"),
entry: ["src/adapter/sqlite/node.ts"],
outDir: "dist/adapter/sqlite",
platform: "node",
metafile: false,
});
await tsup.build({
...baseConfig("sqlite/bun"),
entry: ["src/adapter/sqlite/bun.ts"],
outDir: "dist/adapter/sqlite",
metafile: false,
external: [/^bun\:.*/],
});
} }
await Promise.all([buildApi(), buildUi(), buildUiElements(), buildAdapters()]); await buildApi();
await buildUi();
await buildUiElements();
await buildAdapters();
+1 -2
View File
@@ -2,5 +2,4 @@
#registry = "http://localhost:4873" #registry = "http://localhost:4873"
[test] [test]
coverageSkipTestFiles = true coverageSkipTestFiles = true
console.depth = 10
+6 -6
View File
@@ -17,7 +17,7 @@ async function run(
}); });
// Read from stdout // Read from stdout
const reader = (proc.stdout as ReadableStream).getReader(); const reader = proc.stdout.getReader();
const decoder = new TextDecoder(); const decoder = new TextDecoder();
// Function to read chunks // Function to read chunks
@@ -30,7 +30,7 @@ async function run(
const text = decoder.decode(value); const text = decoder.decode(value);
if (!resolveCalled) { if (!resolveCalled) {
console.info(c.dim(text.replace(/\n$/, ""))); console.log(c.dim(text.replace(/\n$/, "")));
} }
onChunk( onChunk(
text, text,
@@ -189,21 +189,21 @@ const adapters = {
async function testAdapter(name: keyof typeof adapters) { async function testAdapter(name: keyof typeof adapters) {
const config = adapters[name]; const config = adapters[name];
console.info("adapter", c.cyan(name)); console.log("adapter", c.cyan(name));
await config.clean(); await config.clean();
const { proc, data } = await config.start(); const { proc, data } = await config.start();
console.info("proc:", proc.pid, "data:", c.cyan(data)); console.log("proc:", proc.pid, "data:", c.cyan(data));
//proc.kill();process.exit(0); //proc.kill();process.exit(0);
const add_env = "env" in config && config.env ? config.env : ""; const add_env = "env" in config && config.env ? config.env : "";
await $`TEST_URL=${data} TEST_ADAPTER=${name} ${add_env} bun run test:e2e`; await $`TEST_URL=${data} TEST_ADAPTER=${name} ${add_env} bun run test:e2e`;
console.info("DONE!"); console.log("DONE!");
while (!proc.killed) { while (!proc.killed) {
proc.kill("SIGINT"); proc.kill("SIGINT");
await Bun.sleep(250); await Bun.sleep(250);
console.info("Waiting for process to exit..."); console.log("Waiting for process to exit...");
} }
} }
-36
View File
@@ -1,36 +0,0 @@
import { createApp } from "bknd/adapter/bun";
async function generate() {
console.info("Generating MCP documentation...");
const app = await createApp({
config: {
server: {
mcp: {
enabled: true,
path: "/mcp",
},
},
auth: {
enabled: true,
},
media: {
enabled: true,
adapter: {
type: "local",
config: {
path: "./",
},
},
},
},
});
await app.build();
const res = await app.server.request("/mcp?explain=1");
const { tools, resources } = await res.json();
await Bun.write("../docs/mcp.json", JSON.stringify({ tools, resources }, null, 2));
console.info("MCP documentation generated.");
}
void generate();
+34 -26
View File
@@ -3,7 +3,7 @@
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,
"bin": "./dist/cli/index.js", "bin": "./dist/cli/index.js",
"version": "0.18.0", "version": "0.16.0-rc.0",
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.", "description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
"homepage": "https://bknd.io", "homepage": "https://bknd.io",
"repository": { "repository": {
@@ -13,9 +13,8 @@
"bugs": { "bugs": {
"url": "https://github.com/bknd-io/bknd/issues" "url": "https://github.com/bknd-io/bknd/issues"
}, },
"packageManager": "bun@1.2.22",
"engines": { "engines": {
"node": ">=22.13" "node": ">=22"
}, },
"scripts": { "scripts": {
"dev": "BKND_CLI_LOG_LEVEL=debug vite", "dev": "BKND_CLI_LOG_LEVEL=debug vite",
@@ -30,7 +29,7 @@
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly && tsc-alias", "build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly && tsc-alias",
"updater": "bun x npm-check-updates -ui", "updater": "bun x npm-check-updates -ui",
"cli": "LOCAL=1 bun src/cli/index.ts", "cli": "LOCAL=1 bun src/cli/index.ts",
"prepublishOnly": "bun run types && bun run test && bun run test:node && NODE_NO_WARNINGS=1 VITE_DB_URL=:memory: bun run test:e2e && bun run build:all && cp ../README.md ./", "prepublishOnly": "bun run types && bun run test && bun run test:node && bun run test:e2e && bun run build:all && cp ../README.md ./",
"postpublish": "rm -f README.md", "postpublish": "rm -f README.md",
"test": "ALL_TESTS=1 bun test --bail", "test": "ALL_TESTS=1 bun test --bail",
"test:all": "bun run test && bun run test:node", "test:all": "bun run test && bun run test:node",
@@ -40,11 +39,10 @@
"test:coverage": "ALL_TESTS=1 bun test --bail --coverage", "test:coverage": "ALL_TESTS=1 bun test --bail --coverage",
"test:vitest:coverage": "vitest run --coverage", "test:vitest:coverage": "vitest run --coverage",
"test:e2e": "playwright test", "test:e2e": "playwright test",
"test:e2e:adapters": "NODE_NO_WARNINGS=1 bun run e2e/adapters.ts", "test:e2e:adapters": "bun run e2e/adapters.ts",
"test:e2e:ui": "VITE_DB_URL=:memory: playwright test --ui", "test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "VITE_DB_URL=:memory: playwright test --debug", "test:e2e:debug": "playwright test --debug",
"test:e2e:report": "VITE_DB_URL=:memory: playwright show-report", "test:e2e:report": "playwright show-report"
"docs:build-assets": "bun internal/docs.build-assets.ts"
}, },
"license": "FSL-1.1-MIT", "license": "FSL-1.1-MIT",
"dependencies": { "dependencies": {
@@ -62,26 +60,24 @@
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.2",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"fast-xml-parser": "^5.0.8", "fast-xml-parser": "^5.0.8",
"hono": "4.8.3", "hono": "^4.7.11",
"json-schema-form-react": "^0.0.2",
"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.8.4", "kysely": "^0.27.6",
"kysely": "0.27.6",
"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", "radix-ui": "^1.1.3",
"picocolors": "^1.1.1",
"swr": "^2.3.3" "swr": "^2.3.3"
}, },
"devDependencies": { "devDependencies": {
"@aws-sdk/client-s3": "^3.758.0", "@aws-sdk/client-s3": "^3.758.0",
"@bluwy/giget-core": "^0.1.2", "@bluwy/giget-core": "^0.1.2",
"@clack/prompts": "^0.11.0", "@cloudflare/vitest-pool-workers": "^0.8.38",
"@cloudflare/vitest-pool-workers": "^0.9.3",
"@cloudflare/workers-types": "^4.20250606.0", "@cloudflare/workers-types": "^4.20250606.0",
"@dagrejs/dagre": "^1.1.4", "@dagrejs/dagre": "^1.1.4",
"@hono/vite-dev-server": "^0.21.0", "@hono/vite-dev-server": "^0.19.1",
"@hookform/resolvers": "^4.1.3", "@hookform/resolvers": "^4.1.3",
"@libsql/client": "^0.15.9", "@libsql/client": "^0.15.9",
"@mantine/modals": "^7.17.1", "@mantine/modals": "^7.17.1",
@@ -104,11 +100,13 @@
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"jotai": "^2.12.2", "jotai": "^2.12.2",
"jsdom": "^26.0.0", "jsdom": "^26.0.0",
"jsonv-ts": "^0.2.2",
"kysely-d1": "^0.3.0", "kysely-d1": "^0.3.0",
"kysely-generic-sqlite": "^1.2.1", "kysely-generic-sqlite": "^1.2.1",
"libsql-stateless-easy": "^1.8.0", "libsql-stateless-easy": "^1.8.0",
"open": "^10.1.0", "open": "^10.1.0",
"openapi-types": "^12.1.3", "openapi-types": "^12.1.3",
"picocolors": "^1.1.1",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"postcss-preset-mantine": "^1.17.0", "postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1", "postcss-simple-vars": "^7.0.1",
@@ -130,9 +128,7 @@
"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.6.0"
"wrangler": "^4.37.1",
"miniflare": "^4.20250913.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@hono/node-server": "^1.14.3" "@hono/node-server": "^1.14.3"
@@ -165,6 +161,16 @@
"import": "./dist/ui/client/index.js", "import": "./dist/ui/client/index.js",
"require": "./dist/ui/client/index.js" "require": "./dist/ui/client/index.js"
}, },
"./data": {
"types": "./dist/types/data/index.d.ts",
"import": "./dist/data/index.js",
"require": "./dist/data/index.js"
},
"./core": {
"types": "./dist/types/core/index.d.ts",
"import": "./dist/core/index.js",
"require": "./dist/core/index.js"
},
"./utils": { "./utils": {
"types": "./dist/types/core/utils/index.d.ts", "types": "./dist/types/core/utils/index.d.ts",
"import": "./dist/core/utils/index.js", "import": "./dist/core/utils/index.js",
@@ -175,6 +181,11 @@
"import": "./dist/cli/index.js", "import": "./dist/cli/index.js",
"require": "./dist/cli/index.js" "require": "./dist/cli/index.js"
}, },
"./media": {
"types": "./dist/types/media/index.d.ts",
"import": "./dist/media/index.js",
"require": "./dist/media/index.js"
},
"./plugins": { "./plugins": {
"types": "./dist/types/plugins/index.d.ts", "types": "./dist/types/plugins/index.d.ts",
"import": "./dist/plugins/index.js", "import": "./dist/plugins/index.js",
@@ -199,11 +210,6 @@
"import": "./dist/adapter/cloudflare/index.js", "import": "./dist/adapter/cloudflare/index.js",
"require": "./dist/adapter/cloudflare/index.js" "require": "./dist/adapter/cloudflare/index.js"
}, },
"./adapter/cloudflare/proxy": {
"types": "./dist/types/adapter/cloudflare/proxy.d.ts",
"import": "./dist/adapter/cloudflare/proxy.js",
"require": "./dist/adapter/cloudflare/proxy.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"
@@ -245,13 +251,15 @@
}, },
"./dist/main.css": "./dist/ui/main.css", "./dist/main.css": "./dist/ui/main.css",
"./dist/styles.css": "./dist/ui/styles.css", "./dist/styles.css": "./dist/ui/styles.css",
"./dist/manifest.json": "./dist/static/.vite/manifest.json", "./dist/manifest.json": "./dist/static/.vite/manifest.json"
"./static/*": "./dist/static/*"
}, },
"typesVersions": { "typesVersions": {
"*": { "*": {
"data": ["./dist/types/data/index.d.ts"],
"core": ["./dist/types/core/index.d.ts"],
"utils": ["./dist/types/core/utils/index.d.ts"], "utils": ["./dist/types/core/utils/index.d.ts"],
"cli": ["./dist/types/cli/index.d.ts"], "cli": ["./dist/types/cli/index.d.ts"],
"media": ["./dist/types/media/index.d.ts"],
"plugins": ["./dist/types/plugins/index.d.ts"], "plugins": ["./dist/types/plugins/index.d.ts"],
"adapter": ["./dist/types/adapter/index.d.ts"], "adapter": ["./dist/types/adapter/index.d.ts"],
"adapter/cloudflare": ["./dist/types/adapter/cloudflare/index.d.ts"], "adapter/cloudflare": ["./dist/types/adapter/cloudflare/index.d.ts"],
+3 -3
View File
@@ -1,10 +1,10 @@
import type { SafeUser } from "bknd"; import type { SafeUser } from "auth";
import { AuthApi, type AuthApiOptions } from "auth/api/AuthApi"; import { AuthApi, type AuthApiOptions } from "auth/api/AuthApi";
import { DataApi, type DataApiOptions } from "data/api/DataApi"; import { DataApi, type DataApiOptions } from "data/api/DataApi";
import { decode } from "hono/jwt"; import { decode } from "hono/jwt";
import { MediaApi, type MediaApiOptions } from "media/api/MediaApi"; import { MediaApi, type MediaApiOptions } from "media/api/MediaApi";
import { SystemApi } from "modules/SystemApi"; import { SystemApi } from "modules/SystemApi";
import { omitKeys } from "bknd/utils"; import { omitKeys } from "core/utils";
import type { BaseModuleApiOptions } from "modules"; import type { BaseModuleApiOptions } from "modules";
export type TApiUser = SafeUser; export type TApiUser = SafeUser;
@@ -43,7 +43,7 @@ export type ApiOptions = {
} & ( } & (
| { | {
token?: string; token?: string;
user?: TApiUser | null; user?: TApiUser;
} }
| { | {
request: Request; request: Request;
+29 -83
View File
@@ -1,22 +1,21 @@
import type { CreateUserPayload } from "auth/AppAuth"; import type { CreateUserPayload } from "auth/AppAuth";
import { $console, McpClient } from "bknd/utils"; import { $console } from "core/utils";
import { Event } from "core/events"; import { Event } from "core/events";
import type { em as prototypeEm } from "data/prototype"; 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 Modules,
ModuleManager, ModuleManager,
type InitialModuleConfigs,
type ModuleBuildContext, type ModuleBuildContext,
type ModuleConfigs,
type ModuleManagerOptions, type ModuleManagerOptions,
type Modules,
} from "modules/ModuleManager"; } from "modules/ModuleManager";
import { DbModuleManager } from "modules/db/DbModuleManager";
import * as SystemPermissions from "modules/permissions"; import * as SystemPermissions from "modules/permissions";
import { AdminController, type AdminControllerOptions } from "modules/server/AdminController"; import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
import { SystemController } from "modules/server/SystemController"; import { SystemController } from "modules/server/SystemController";
import type { MaybePromise, PartialRec } from "core/types"; import type { MaybePromise } from "core/types";
import type { ServerEnv } from "modules/Controller"; import type { ServerEnv } from "modules/Controller";
import type { IEmailDriver, ICacheDriver } from "core/drivers"; import type { IEmailDriver, ICacheDriver } from "core/drivers";
@@ -24,34 +23,14 @@ import type { IEmailDriver, ICacheDriver } from "core/drivers";
import { Api, type ApiOptions } from "Api"; import { Api, type ApiOptions } from "Api";
export type AppPluginConfig = { export type AppPluginConfig = {
/**
* The name of the plugin.
*/
name: string; name: string;
/**
* The schema of the plugin.
*/
schema?: () => MaybePromise<ReturnType<typeof prototypeEm> | void>; schema?: () => MaybePromise<ReturnType<typeof prototypeEm> | void>;
/**
* Called before the app is built.
*/
beforeBuild?: () => MaybePromise<void>; beforeBuild?: () => MaybePromise<void>;
/**
* Called after the app is built.
*/
onBuilt?: () => MaybePromise<void>; onBuilt?: () => MaybePromise<void>;
/**
* Called when the server is initialized.
*/
onServerInit?: (server: Hono<ServerEnv>) => MaybePromise<void>; onServerInit?: (server: Hono<ServerEnv>) => MaybePromise<void>;
/**
* Called when the app is booted.
*/
onBoot?: () => MaybePromise<void>;
/**
* Called when the app is first booted.
*/
onFirstBoot?: () => MaybePromise<void>; onFirstBoot?: () => MaybePromise<void>;
onBoot?: () => MaybePromise<void>;
onModulesCreated?: (modules: Modules) => void;
}; };
export type AppPlugin = (app: App) => AppPluginConfig; export type AppPlugin = (app: App) => AppPluginConfig;
@@ -62,9 +41,6 @@ export class AppConfigUpdatedEvent extends AppEvent<{
}> { }> {
static override slug = "app-config-updated"; static override slug = "app-config-updated";
} }
/**
* @type {Event<{ app: App }>}
*/
export class AppBuiltEvent extends AppEvent { export class AppBuiltEvent extends AppEvent {
static override slug = "app-built"; static override slug = "app-built";
} }
@@ -94,23 +70,17 @@ export type AppOptions = {
email?: IEmailDriver; email?: IEmailDriver;
cache?: ICacheDriver; cache?: ICacheDriver;
}; };
mode?: "db" | "code";
readonly?: boolean;
}; };
export type CreateAppConfig = { export type CreateAppConfig = {
connection?: Connection | { url: string }; connection?: Connection | { url: string };
config?: PartialRec<ModuleConfigs>; initialConfig?: InitialModuleConfigs;
options?: AppOptions; options?: AppOptions;
}; };
export type AppConfig = { version: number } & ModuleConfigs; export type AppConfig = InitialModuleConfigs;
export type LocalApiOptions = Request | ApiOptions; export type LocalApiOptions = Request | ApiOptions;
export class App< export class App<C extends Connection = Connection, Options extends AppOptions = AppOptions> {
C extends Connection = Connection,
Config extends PartialRec<ModuleConfigs> = PartialRec<ModuleConfigs>,
Options extends AppOptions = AppOptions,
> {
static readonly Events = AppEvents; static readonly Events = AppEvents;
modules: ModuleManager; modules: ModuleManager;
@@ -121,12 +91,11 @@ export class App<
private trigger_first_boot = false; private trigger_first_boot = false;
private _building: boolean = false; private _building: boolean = false;
private _systemController: SystemController | null = null;
constructor( constructor(
public connection: C, public connection: C,
_config?: Config, _initialConfig?: InitialModuleConfigs,
public options?: Options, private options?: Options,
) { ) {
this.drivers = options?.drivers ?? {}; this.drivers = options?.drivers ?? {};
@@ -138,29 +107,18 @@ export class App<
this.plugins.set(config.name, config); this.plugins.set(config.name, config);
} }
this.runPlugins("onBoot"); this.runPlugins("onBoot");
this.modules = new ModuleManager(connection, {
// use db manager by default
const Manager = this.mode === "db" ? DbModuleManager : ModuleManager;
this.modules = new Manager(connection, {
...(options?.manager ?? {}), ...(options?.manager ?? {}),
initial: _config, initial: _initialConfig,
onUpdated: this.onUpdated.bind(this), onUpdated: this.onUpdated.bind(this),
onFirstBoot: this.onFirstBoot.bind(this), onFirstBoot: this.onFirstBoot.bind(this),
onServerInit: this.onServerInit.bind(this), onServerInit: this.onServerInit.bind(this),
onModulesBuilt: this.onModulesBuilt.bind(this), onModulesBuilt: this.onModulesBuilt.bind(this),
onModulesCreated: this.onModulesCreated.bind(this),
}); });
this.modules.ctx().emgr.registerEvents(AppEvents); this.modules.ctx().emgr.registerEvents(AppEvents);
} }
get mode() {
return this.options?.mode ?? "db";
}
isReadOnly() {
return Boolean(this.mode === "code" || this.options?.readonly);
}
get emgr() { get emgr() {
return this.modules.ctx().emgr; return this.modules.ctx().emgr;
} }
@@ -191,7 +149,7 @@ export class App<
return results as any; return results as any;
} }
async build(options?: { sync?: boolean; forceBuild?: boolean; [key: string]: any }) { async build(options?: { sync?: boolean; fetch?: boolean; forceBuild?: boolean }) {
// prevent multiple concurrent builds // prevent multiple concurrent builds
if (this._building) { if (this._building) {
while (this._building) { while (this._building) {
@@ -204,14 +162,13 @@ export class App<
this._building = true; this._building = true;
if (options?.sync) this.modules.ctx().flags.sync_required = true; if (options?.sync) this.modules.ctx().flags.sync_required = true;
await this.modules.build(); await this.modules.build({ fetch: options?.fetch });
const { guard } = this.modules.ctx(); const { guard, server } = this.modules.ctx();
// load system controller // load system controller
guard.registerPermissions(Object.values(SystemPermissions)); guard.registerPermissions(Object.values(SystemPermissions));
this._systemController = new SystemController(this); server.route("/api/system", new SystemController(this).getController());
this._systemController.register(this);
// emit built event // emit built event
$console.log("App built"); $console.log("App built");
@@ -231,6 +188,10 @@ export class App<
this._building = false; this._building = false;
} }
mutateConfig<Module extends keyof Modules>(module: Module) {
return this.modules.mutateConfigSafe(module);
}
get server() { get server() {
return this.modules.server; return this.modules.server;
} }
@@ -239,15 +200,7 @@ export class App<
return this.modules.ctx().em; return this.modules.ctx().em;
} }
get mcp() {
return this._systemController?._mcpServer;
}
get fetch(): Hono["fetch"] { get fetch(): Hono["fetch"] {
if (!this.isBuilt()) {
throw new Error("App is not built yet, run build() first");
}
return this.server.fetch as any; return this.server.fetch as any;
} }
@@ -305,18 +258,6 @@ export class App<
return new Api({ host: "http://localhost", ...(options ?? {}), fetcher }); return new Api({ host: "http://localhost", ...(options ?? {}), fetcher });
} }
getMcpClient() {
const config = this.modules.get("server").config.mcp;
if (!config.enabled) {
throw new Error("MCP is not enabled");
}
return new McpClient({
url: "http://localhost" + config.path,
fetch: this.server.request,
});
}
async onUpdated<Module extends keyof Modules>(module: Module, config: ModuleConfigs[Module]) { async onUpdated<Module extends keyof Modules>(module: Module, config: ModuleConfigs[Module]) {
// if the EventManager was disabled, we assume we shouldn't // if the EventManager was disabled, we assume we shouldn't
// respond to events, such as "onUpdated". // respond to events, such as "onUpdated".
@@ -386,6 +327,11 @@ export class App<
} }
} }
} }
protected async onModulesCreated(modules: Modules, ctx: ModuleBuildContext) {
await this.runPlugins("onModulesCreated", modules, ctx);
this.options?.manager?.onModulesCreated?.(modules, ctx);
}
} }
export function createApp(config: CreateAppConfig = {}) { export function createApp(config: CreateAppConfig = {}) {
@@ -393,5 +339,5 @@ export function createApp(config: CreateAppConfig = {}) {
throw new Error("Invalid connection"); throw new Error("Invalid connection");
} }
return new App(config.connection, config.config, config.options); return new App(config.connection, config.initialConfig, config.options);
} }
+20 -13
View File
@@ -1,7 +1,6 @@
import type { TestRunner } from "core/test"; import type { TestRunner } from "core/test";
import type { BkndConfig, DefaultArgs } from "./index"; import type { BkndConfig, DefaultArgs, FrameworkOptions, RuntimeOptions } from "./index";
import type { App } from "App"; import type { App } from "App";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
export function adapterTestSuite< export function adapterTestSuite<
Config extends BkndConfig = BkndConfig, Config extends BkndConfig = BkndConfig,
@@ -14,17 +13,24 @@ export function adapterTestSuite<
label = "app", label = "app",
overrides = {}, overrides = {},
}: { }: {
makeApp: (config: Config, args?: Args) => Promise<App>; makeApp: (
makeHandler?: (config?: Config, args?: Args) => (request: Request) => Promise<Response>; config: Config,
args?: Args,
opts?: RuntimeOptions | FrameworkOptions,
) => Promise<App>;
makeHandler?: (
config?: Config,
args?: Args,
opts?: RuntimeOptions | FrameworkOptions,
) => (request: Request) => Promise<Response>;
label?: string; label?: string;
overrides?: { overrides?: {
dbUrl?: string; dbUrl?: string;
}; };
}, },
) { ) {
const { test, expect, mock, beforeAll, afterAll } = testRunner; const { test, expect, mock } = testRunner;
beforeAll(() => disableConsoleLog()); const id = crypto.randomUUID();
afterAll(() => enableConsoleLog());
test(`creates ${label}`, async () => { test(`creates ${label}`, async () => {
const beforeBuild = mock(async () => null) as any; const beforeBuild = mock(async () => null) as any;
@@ -33,7 +39,7 @@ export function adapterTestSuite<
const config = { const config = {
app: (env) => ({ app: (env) => ({
connection: { url: env.url }, connection: { url: env.url },
config: { initialConfig: {
server: { cors: { origin: env.origin } }, server: { cors: { origin: env.origin } },
}, },
}), }),
@@ -47,10 +53,11 @@ export function adapterTestSuite<
url: overrides.dbUrl ?? ":memory:", url: overrides.dbUrl ?? ":memory:",
origin: "localhost", origin: "localhost",
} as any, } as any,
{ id },
); );
expect(app).toBeDefined(); expect(app).toBeDefined();
expect(app.toJSON().server.cors.origin).toEqual("localhost"); expect(app.toJSON().server.cors.origin).toEqual("localhost");
expect(beforeBuild).toHaveBeenCalledTimes(2); expect(beforeBuild).toHaveBeenCalledTimes(1);
expect(onBuilt).toHaveBeenCalledTimes(1); expect(onBuilt).toHaveBeenCalledTimes(1);
}); });
@@ -61,7 +68,7 @@ export function adapterTestSuite<
return { res, data }; return { res, data };
}; };
/* test.skip("responds with the same app id", async () => { test("responds with the same app id", async () => {
const fetcher = makeHandler(undefined, undefined, { id }); const fetcher = makeHandler(undefined, undefined, { id });
const { res, data } = await getConfig(fetcher); const { res, data } = await getConfig(fetcher);
@@ -70,14 +77,14 @@ export function adapterTestSuite<
expect(data.server.cors.origin).toEqual("localhost"); expect(data.server.cors.origin).toEqual("localhost");
}); });
test.skip("creates fresh & responds to api config", async () => { test("creates fresh & responds to api config", async () => {
// set the same id, but force recreate // set the same id, but force recreate
const fetcher = makeHandler(undefined, undefined, { id }); const fetcher = makeHandler(undefined, undefined, { id, force: true });
const { res, data } = await getConfig(fetcher); const { res, data } = await getConfig(fetcher);
expect(res.ok).toBe(true); expect(res.ok).toBe(true);
expect(res.status).toBe(200); expect(res.status).toBe(200);
expect(data.server.cors.origin).toEqual("*"); expect(data.server.cors.origin).toEqual("*");
}); */ });
} }
} }
+1 -1
View File
@@ -10,6 +10,6 @@ afterAll(enableConsoleLog);
describe("astro adapter", () => { describe("astro adapter", () => {
adapterTestSuite(bunTestRunner, { adapterTestSuite(bunTestRunner, {
makeApp: astro.getApp, makeApp: astro.getApp,
makeHandler: (c, a) => (request: Request) => astro.serve(c, a)({ request }), makeHandler: (c, a, o) => (request: Request) => astro.serve(c, a, o)({ request }),
}); });
}); });
+9 -4
View File
@@ -1,4 +1,4 @@
import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter"; import { type FrameworkBkndConfig, createFrameworkApp, type FrameworkOptions } from "bknd/adapter";
type AstroEnv = NodeJS.ProcessEnv; type AstroEnv = NodeJS.ProcessEnv;
type TAstro = { type TAstro = {
@@ -9,12 +9,17 @@ export type AstroBkndConfig<Env = AstroEnv> = FrameworkBkndConfig<Env>;
export async function getApp<Env = AstroEnv>( export async function getApp<Env = AstroEnv>(
config: AstroBkndConfig<Env> = {}, config: AstroBkndConfig<Env> = {},
args: Env = {} as Env, args: Env = {} as Env,
opts: FrameworkOptions = {},
) { ) {
return await createFrameworkApp(config, args ?? import.meta.env); return await createFrameworkApp(config, args ?? import.meta.env, opts);
} }
export function serve<Env = AstroEnv>(config: AstroBkndConfig<Env> = {}, args: Env = {} as Env) { export function serve<Env = AstroEnv>(
config: AstroBkndConfig<Env> = {},
args: Env = {} as Env,
opts?: FrameworkOptions,
) {
return async (fnArgs: TAstro) => { return async (fnArgs: TAstro) => {
return (await getApp(config, args)).fetch(fnArgs.request); return (await getApp(config, args, opts)).fetch(fnArgs.request);
}; };
} }
+5 -2
View File
@@ -1,7 +1,7 @@
import type { App } from "bknd"; import type { App } from "bknd";
import { handle } from "hono/aws-lambda"; import { handle } from "hono/aws-lambda";
import { serveStatic } from "@hono/node-server/serve-static"; import { serveStatic } from "@hono/node-server/serve-static";
import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter"; import { type RuntimeBkndConfig, createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
type AwsLambdaEnv = object; type AwsLambdaEnv = object;
export type AwsLambdaBkndConfig<Env extends AwsLambdaEnv = AwsLambdaEnv> = export type AwsLambdaBkndConfig<Env extends AwsLambdaEnv = AwsLambdaEnv> =
@@ -20,6 +20,7 @@ export type AwsLambdaBkndConfig<Env extends AwsLambdaEnv = AwsLambdaEnv> =
export async function createApp<Env extends AwsLambdaEnv = AwsLambdaEnv>( export async function createApp<Env extends AwsLambdaEnv = AwsLambdaEnv>(
{ adminOptions = false, assets, ...config }: AwsLambdaBkndConfig<Env> = {}, { adminOptions = false, assets, ...config }: AwsLambdaBkndConfig<Env> = {},
args: Env = {} as Env, args: Env = {} as Env,
opts?: RuntimeOptions,
): Promise<App> { ): Promise<App> {
let additional: Partial<RuntimeBkndConfig> = { let additional: Partial<RuntimeBkndConfig> = {
adminOptions, adminOptions,
@@ -56,15 +57,17 @@ export async function createApp<Env extends AwsLambdaEnv = AwsLambdaEnv>(
...additional, ...additional,
}, },
args ?? process.env, args ?? process.env,
opts,
); );
} }
export function serve<Env extends AwsLambdaEnv = AwsLambdaEnv>( export function serve<Env extends AwsLambdaEnv = AwsLambdaEnv>(
config: AwsLambdaBkndConfig<Env> = {}, config: AwsLambdaBkndConfig<Env> = {},
args: Env = {} as Env, args: Env = {} as Env,
opts?: RuntimeOptions,
) { ) {
return async (event) => { return async (event) => {
const app = await createApp(config, args); const app = await createApp(config, args, opts);
return await handle(app.server)(event); return await handle(app.server)(event);
}; };
} }
+2 -2
View File
@@ -11,8 +11,8 @@ describe("aws adapter", () => {
adapterTestSuite(bunTestRunner, { adapterTestSuite(bunTestRunner, {
makeApp: awsLambda.createApp, makeApp: awsLambda.createApp,
// @todo: add a request to lambda event translator? // @todo: add a request to lambda event translator?
makeHandler: (c, a) => async (request: Request) => { makeHandler: (c, a, o) => async (request: Request) => {
const app = await awsLambda.createApp(c, a); const app = await awsLambda.createApp(c, a, o);
return app.fetch(request); return app.fetch(request);
}, },
}); });
+16 -15
View File
@@ -1,43 +1,44 @@
/// <reference types="bun-types" /> /// <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, type RuntimeOptions } from "bknd/adapter";
import { registerLocalMediaAdapter } from "."; import { registerLocalMediaAdapter } from ".";
import { config, type App } from "bknd"; import { config } from "bknd/core";
import type { ServeOptions } from "bun"; import type { ServeOptions } from "bun";
import { serveStatic } from "hono/bun"; import { serveStatic } from "hono/bun";
import type { App } from "App";
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<ServeOptions, "fetch">;
export async function createApp<Env = BunEnv>( export async function createApp<Env = BunEnv>(
{ distPath, serveStatic: _serveStatic, ...config }: BunBkndConfig<Env> = {}, { distPath, ...config }: BunBkndConfig<Env> = {},
args: Env = {} as Env, args: Env = {} as Env,
opts?: RuntimeOptions,
) { ) {
const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static"); const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static");
registerLocalMediaAdapter();
return await createRuntimeApp( const app = await createRuntimeApp(
{ {
serveStatic:
_serveStatic ??
serveStatic({
root,
}),
...config, ...config,
serveStatic: serveStatic({ root }),
}, },
args ?? (process.env as Env), args ?? (process.env as Env),
opts,
); );
registerLocalMediaAdapter(app);
return app;
} }
export function createHandler<Env = BunEnv>( export function createHandler<Env = BunEnv>(
config: BunBkndConfig<Env> = {}, config: BunBkndConfig<Env> = {},
args: Env = {} as Env, args: Env = {} as Env,
opts?: RuntimeOptions,
) { ) {
let app: App | undefined; let app: App | undefined;
return async (req: Request) => { return async (req: Request) => {
if (!app) { if (!app) {
app = await createApp(config, args ?? (process.env as Env)); app = await createApp(config, args ?? (process.env as Env), opts);
} }
return app.fetch(req); return app.fetch(req);
}; };
@@ -47,16 +48,16 @@ export function serve<Env = BunEnv>(
{ {
distPath, distPath,
connection, connection,
config: _config, initialConfig,
options, options,
port = config.server.default_port, port = config.server.default_port,
onBuilt, onBuilt,
buildConfig, buildConfig,
adminOptions, adminOptions,
serveStatic,
...serveOptions ...serveOptions
}: BunBkndConfig<Env> = {}, }: BunBkndConfig<Env> = {},
args: Env = {} as Env, args: Env = {} as Env,
opts?: RuntimeOptions,
) { ) {
Bun.serve({ Bun.serve({
...serveOptions, ...serveOptions,
@@ -64,15 +65,15 @@ export function serve<Env = BunEnv>(
fetch: createHandler( fetch: createHandler(
{ {
connection, connection,
config: _config, initialConfig,
options, options,
onBuilt, onBuilt,
buildConfig, buildConfig,
adminOptions, adminOptions,
distPath, distPath,
serveStatic,
}, },
args, args,
opts,
), ),
}); });
@@ -1,9 +1,8 @@
import { connectionTestSuite } from "data/connection/connection-test-suite"; import { connectionTestSuite } from "data/connection/connection-test-suite";
import { bunSqlite } from "./BunSqliteConnection"; import { bunSqlite } from "./BunSqliteConnection";
import { bunTestRunner } from "adapter/bun/test"; import { bunTestRunner } from "adapter/bun/test";
import { describe, test, mock, expect } from "bun:test"; import { describe } from "bun:test";
import { Database } from "bun:sqlite"; import { Database } from "bun:sqlite";
import { GenericSqliteConnection } from "data/connection/sqlite/GenericSqliteConnection";
describe("BunSqliteConnection", () => { describe("BunSqliteConnection", () => {
connectionTestSuite(bunTestRunner, { connectionTestSuite(bunTestRunner, {
@@ -13,20 +12,4 @@ describe("BunSqliteConnection", () => {
}), }),
rawDialectDetails: [], rawDialectDetails: [],
}); });
test("onCreateConnection", async () => {
const called = mock(() => null);
const conn = bunSqlite({
onCreateConnection: (db) => {
expect(db).toBeInstanceOf(Database);
called();
},
});
await conn.ping();
expect(conn).toBeInstanceOf(GenericSqliteConnection);
expect(conn.db).toBeInstanceOf(Database);
expect(called).toHaveBeenCalledTimes(1);
});
}); });
@@ -1,53 +1,40 @@
import { Database } from "bun:sqlite"; import { Database } from "bun:sqlite";
import { import { genericSqlite, type GenericSqliteConnection } from "bknd/data";
genericSqlite,
type GenericSqliteConnection,
type GenericSqliteConnectionConfig,
} from "bknd";
import { omitKeys } from "bknd/utils";
export type BunSqliteConnection = GenericSqliteConnection<Database>; export type BunSqliteConnection = GenericSqliteConnection<Database>;
export type BunSqliteConnectionConfig = Omit< export type BunSqliteConnectionConfig = {
GenericSqliteConnectionConfig<Database>, database: Database;
"name" | "supports" };
> &
({ database?: Database; url?: never } | { url?: string; database?: never });
export function bunSqlite(config?: BunSqliteConnectionConfig) { export function bunSqlite(config?: BunSqliteConnectionConfig | { url: string }) {
let db: Database | undefined; let db: Database;
if (config) { if (config) {
if ("database" in config && config.database) { if ("database" in config) {
db = config.database; db = config.database;
} else if (config.url) { } else {
db = new Database(config.url); db = new Database(config.url);
} }
} } else {
if (!db) {
db = new Database(":memory:"); db = new Database(":memory:");
} }
return genericSqlite( return genericSqlite("bun-sqlite", db, (utils) => {
"bun-sqlite", //const fn = cache ? "query" : "prepare";
db, const getStmt = (sql: string) => db.prepare(sql);
(utils) => {
const getStmt = (sql: string) => db.prepare(sql);
return { return {
db, db,
query: utils.buildQueryFn({ query: utils.buildQueryFn({
all: (sql, parameters) => getStmt(sql).all(...(parameters || [])), all: (sql, parameters) => getStmt(sql).all(...(parameters || [])),
run: (sql, parameters) => { run: (sql, parameters) => {
const { changes, lastInsertRowid } = getStmt(sql).run(...(parameters || [])); const { changes, lastInsertRowid } = getStmt(sql).run(...(parameters || []));
return { return {
insertId: utils.parseBigInt(lastInsertRowid), insertId: utils.parseBigInt(lastInsertRowid),
numAffectedRows: utils.parseBigInt(changes), numAffectedRows: utils.parseBigInt(changes),
}; };
}, },
}), }),
close: () => db.close(), close: () => db.close(),
}; };
}, });
omitKeys(config ?? ({} as any), ["database", "url", "name", "supports"]),
);
} }
+1 -2
View File
@@ -1,4 +1,4 @@
import { expect, test, mock, describe, beforeEach, afterEach, afterAll, beforeAll } from "bun:test"; import { expect, test, mock, describe, beforeEach, afterEach, afterAll } from "bun:test";
export const bunTestRunner = { export const bunTestRunner = {
describe, describe,
@@ -8,5 +8,4 @@ export const bunTestRunner = {
beforeEach, beforeEach,
afterEach, afterEach,
afterAll, afterAll,
beforeAll,
}; };
+1 -7
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;
@@ -14,11 +12,7 @@ export function getBindings<T extends GetBindingType>(env: any, type: T): Bindin
const bindings: BindingMap<T>[] = []; const bindings: BindingMap<T>[] = [];
for (const key in env) { for (const key in env) {
try { try {
if ( if (env[key] && (env[key] as any).constructor.name === type) {
(env[key] as any).constructor.name === type ||
String(env[key]) === `[object ${type}]` ||
inspect(env[key]).includes(type)
) {
bindings.push({ bindings.push({
key, key,
value: env[key] as BindingTypeMap[T], value: env[key] as BindingTypeMap[T],
@@ -1,12 +1,13 @@
import { afterAll, beforeAll, describe, expect, it } from "bun:test"; import { afterAll, beforeAll, describe, expect, it } from "bun:test";
import { makeConfig, type CloudflareContext } from "./config"; import { makeApp } from "./modes/fresh";
import { makeConfig, type CfMakeConfigArgs } from "./config";
import { disableConsoleLog, enableConsoleLog } from "core/utils"; import { disableConsoleLog, enableConsoleLog } from "core/utils";
import { adapterTestSuite } from "adapter/adapter-test-suite"; import { adapterTestSuite } from "adapter/adapter-test-suite";
import { bunTestRunner } from "adapter/bun/test"; import { bunTestRunner } from "adapter/bun/test";
import { type CloudflareBkndConfig, createApp } from "./cloudflare-workers.adapter"; import type { CloudflareBkndConfig } from "./cloudflare-workers.adapter";
/* beforeAll(disableConsoleLog); beforeAll(disableConsoleLog);
afterAll(enableConsoleLog); */ afterAll(enableConsoleLog);
describe("cf adapter", () => { describe("cf adapter", () => {
const DB_URL = ":memory:"; const DB_URL = ":memory:";
@@ -17,42 +18,42 @@ describe("cf adapter", () => {
}); });
it("makes config", async () => { it("makes config", async () => {
const staticConfig = await makeConfig( const staticConfig = makeConfig(
{ {
connection: { url: DB_URL }, connection: { url: DB_URL },
config: { data: { basepath: DB_URL } }, initialConfig: { data: { basepath: DB_URL } },
}, },
$ctx({ DB_URL }), $ctx({ DB_URL }),
); );
expect(staticConfig.config).toEqual({ data: { basepath: DB_URL } }); expect(staticConfig.initialConfig).toEqual({ data: { basepath: DB_URL } });
expect(staticConfig.connection).toBeDefined(); expect(staticConfig.connection).toBeDefined();
const dynamicConfig = await makeConfig( const dynamicConfig = makeConfig(
{ {
app: (env) => ({ app: (env) => ({
config: { data: { basepath: env.DB_URL } }, initialConfig: { data: { basepath: env.DB_URL } },
connection: { url: env.DB_URL }, connection: { url: env.DB_URL },
}), }),
}, },
$ctx({ DB_URL }), $ctx({ DB_URL }),
); );
expect(dynamicConfig.config).toEqual({ data: { basepath: DB_URL } }); expect(dynamicConfig.initialConfig).toEqual({ data: { basepath: DB_URL } });
expect(dynamicConfig.connection).toBeDefined(); expect(dynamicConfig.connection).toBeDefined();
}); });
adapterTestSuite<CloudflareBkndConfig, CloudflareContext<any>>(bunTestRunner, { adapterTestSuite<CloudflareBkndConfig, CfMakeConfigArgs<any>>(bunTestRunner, {
makeApp: async (c, a) => { makeApp: async (c, a, o) => {
return await createApp(c, { env: a } as any); return await makeApp(c, { env: a } as any, o);
}, },
makeHandler: (c, a) => { makeHandler: (c, a, o) => {
console.log("args", a);
return async (request: any) => { return async (request: any) => {
const app = await createApp( const app = await makeApp(
// needs a fallback, otherwise tries to launch D1 // needs a fallback, otherwise tries to launch D1
c ?? { c ?? {
connection: { url: DB_URL }, connection: { url: DB_URL },
}, },
a as any, a!,
o,
); );
return app.fetch(request); return app.fetch(request);
}; };
@@ -3,10 +3,12 @@
import type { RuntimeBkndConfig } from "bknd/adapter"; import type { RuntimeBkndConfig } from "bknd/adapter";
import { Hono } from "hono"; import { Hono } from "hono";
import { serveStatic } from "hono/cloudflare-workers"; import { serveStatic } from "hono/cloudflare-workers";
import type { MaybePromise } from "bknd"; import { getFresh } from "./modes/fresh";
import { $console } from "bknd/utils"; import { getCached } from "./modes/cached";
import { createRuntimeApp } from "bknd/adapter"; import { getDurable } from "./modes/durable";
import { registerAsyncsExecutionContext, makeConfig, type CloudflareContext } from "./config"; import type { App } from "bknd";
import { $console } from "core/utils";
import { registerMedia } from "./storage/StorageR2Adapter";
declare global { declare global {
namespace Cloudflare { namespace Cloudflare {
@@ -16,10 +18,12 @@ declare global {
export type CloudflareEnv = Cloudflare.Env; export type CloudflareEnv = Cloudflare.Env;
export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> & { export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> & {
bindings?: (args: Env) => MaybePromise<{ mode?: "warm" | "fresh" | "cache" | "durable";
bindings?: (args: Env) => {
kv?: KVNamespace; kv?: KVNamespace;
dobj?: DurableObjectNamespace;
db?: D1Database; db?: D1Database;
}>; };
d1?: { d1?: {
session?: boolean; session?: boolean;
transport?: "header" | "cookie"; transport?: "header" | "cookie";
@@ -30,30 +34,14 @@ export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> &
keepAliveSeconds?: number; keepAliveSeconds?: number;
forceHttps?: boolean; forceHttps?: boolean;
manifest?: string; manifest?: string;
registerMedia?: boolean | ((env: Env) => void); registerMedia?: boolean;
}; };
export async function createApp<Env extends CloudflareEnv = CloudflareEnv>( export type Context<Env = CloudflareEnv> = {
config: CloudflareBkndConfig<Env> = {}, request: Request;
ctx: Partial<CloudflareContext<Env>> = {}, env: Env;
) { ctx: ExecutionContext;
const appConfig = await makeConfig( };
{
...config,
onBuilt: async (app) => {
if (ctx.ctx) {
registerAsyncsExecutionContext(app, ctx?.ctx);
}
await config.onBuilt?.(app);
},
},
ctx,
);
return await createRuntimeApp<Env>(appConfig, ctx?.env);
}
// compatiblity
export const getFresh = createApp;
export function serve<Env extends CloudflareEnv = CloudflareEnv>( export function serve<Env extends CloudflareEnv = CloudflareEnv>(
config: CloudflareBkndConfig<Env> = {}, config: CloudflareBkndConfig<Env> = {},
@@ -92,10 +80,36 @@ export function serve<Env extends CloudflareEnv = CloudflareEnv>(
} }
} }
const context = { request, env, ctx } as CloudflareContext<Env>; const context = { request, env, ctx } as Context<Env>;
const app = await createApp(config, context); const mode = config.mode ?? "warm";
let app: App;
switch (mode) {
case "fresh":
app = await getFresh(config, context, { force: true });
break;
case "warm":
app = await getFresh(config, context);
break;
case "cache":
app = await getCached(config, context);
break;
case "durable":
return await getDurable(config, context);
default:
throw new Error(`Unknown mode ${mode}`);
}
registerMediaInternal(app, config, context);
return app.fetch(request, env, ctx); return app.fetch(request, env, ctx);
}, },
}; };
} }
let media_registered: boolean = false;
function registerMediaInternal(app: App, config: CloudflareBkndConfig<any>, ctx?: Context) {
if (!media_registered && config.registerMedia !== false) {
registerMedia(app, ctx?.env as any);
media_registered = true;
}
}
+14 -25
View File
@@ -1,16 +1,15 @@
/// <reference types="@cloudflare/workers-types" /> /// <reference types="@cloudflare/workers-types" />
import { Connection } from "bknd";
import { sqlite } from "bknd/adapter/sqlite";
import { makeConfig as makeAdapterConfig } from "bknd/adapter";
import { registerMedia } from "./storage/StorageR2Adapter";
import { getBinding } from "./bindings"; import { getBinding } from "./bindings";
import { d1Sqlite } from "./connection/D1Connection"; import { d1Sqlite } from "./connection/D1Connection";
import { Connection } from "bknd/data";
import type { CloudflareBkndConfig, CloudflareEnv } from "."; import type { CloudflareBkndConfig, CloudflareEnv } from ".";
import { App } from "bknd"; import { App } from "bknd";
import type { Context as HonoContext, ExecutionContext } from "hono"; import { makeConfig as makeAdapterConfig } from "bknd/adapter";
import { $console } from "bknd/utils"; import type { Context, ExecutionContext } from "hono";
import { $console } from "core/utils";
import { setCookie } from "hono/cookie"; import { setCookie } from "hono/cookie";
import { sqlite } from "bknd/adapter/sqlite";
export const constants = { export const constants = {
exec_async_event_id: "cf_register_waituntil", exec_async_event_id: "cf_register_waituntil",
@@ -22,10 +21,10 @@ export const constants = {
}, },
}; };
export type CloudflareContext<Env extends CloudflareEnv = CloudflareEnv> = { export type CfMakeConfigArgs<Env extends CloudflareEnv = CloudflareEnv> = {
env: Env; env: Env;
ctx: ExecutionContext; ctx?: ExecutionContext;
request: Request; request?: Request;
}; };
function getCookieValue(cookies: string | null, name: string) { function getCookieValue(cookies: string | null, name: string) {
@@ -67,7 +66,7 @@ export function d1SessionHelper(config: CloudflareBkndConfig<any>) {
return undefined; return undefined;
}, },
set: (c: HonoContext, d1?: D1DatabaseSession) => { set: (c: Context, d1?: D1DatabaseSession) => {
if (!d1 || !config.d1?.session) return; if (!d1 || !config.d1?.session) return;
const session = d1.getBookmark(); const session = d1.getBookmark();
@@ -88,21 +87,11 @@ export function d1SessionHelper(config: CloudflareBkndConfig<any>) {
}; };
} }
let media_registered: boolean = false; export function makeConfig<Env extends CloudflareEnv = CloudflareEnv>(
export async function makeConfig<Env extends CloudflareEnv = CloudflareEnv>(
config: CloudflareBkndConfig<Env>, config: CloudflareBkndConfig<Env>,
args?: Partial<CloudflareContext<Env>>, args?: CfMakeConfigArgs<Env>,
) { ) {
if (!media_registered && config.registerMedia !== false) { const appConfig = makeAdapterConfig(config, args?.env);
if (typeof config.registerMedia === "function") {
config.registerMedia(args?.env as any);
} else {
registerMedia(args?.env as any);
}
media_registered = true;
}
const appConfig = await makeAdapterConfig(config, args?.env);
// if connection instance is given, don't do anything // if connection instance is given, don't do anything
// other than checking if D1 session is defined // other than checking if D1 session is defined
@@ -115,12 +104,12 @@ export async function makeConfig<Env extends CloudflareEnv = CloudflareEnv>(
} }
// if connection is given, try to open with unified sqlite adapter // if connection is given, try to open with unified sqlite adapter
} else if (appConfig.connection) { } else if (appConfig.connection) {
appConfig.connection = sqlite(appConfig.connection) as any; appConfig.connection = sqlite(appConfig.connection);
// if connection is not given, but env is set // if connection is not given, but env is set
// try to make D1 from bindings // try to make D1 from bindings
} else if (args?.env) { } else if (args?.env) {
const bindings = await config.bindings?.(args?.env); const bindings = config.bindings?.(args?.env);
const sessionHelper = d1SessionHelper(config); const sessionHelper = d1SessionHelper(config);
const sessionId = sessionHelper.get(args.request); const sessionId = sessionHelper.get(args.request);
let session: D1DatabaseSession | undefined; let session: D1DatabaseSession | undefined;
@@ -1,6 +1,6 @@
/// <reference types="@cloudflare/workers-types" /> /// <reference types="@cloudflare/workers-types" />
import { genericSqlite, type GenericSqliteConnection } from "bknd"; import { genericSqlite, type GenericSqliteConnection } from "bknd/data";
import type { QueryResult } from "kysely"; import type { QueryResult } from "kysely";
export type D1SqliteConnection = GenericSqliteConnection<D1Database>; export type D1SqliteConnection = GenericSqliteConnection<D1Database>;
@@ -1,18 +1,18 @@
/// <reference types="@cloudflare/workers-types" /> /// <reference types="@cloudflare/workers-types" />
import { genericSqlite, type GenericSqliteConnection } from "bknd"; import { genericSqlite, type GenericSqliteConnection } from "bknd/data";
import type { QueryResult } from "kysely"; import type { QueryResult } from "kysely";
export type DoSqliteConnection = GenericSqliteConnection<DurableObjectState["storage"]["sql"]>; export type D1SqliteConnection = GenericSqliteConnection<D1Database>;
export type DurableObjecSql = DurableObjectState["storage"]["sql"]; export type DurableObjecSql = DurableObjectState["storage"]["sql"];
export type DoConnectionConfig<DB extends DurableObjecSql> = export type D1ConnectionConfig<DB extends DurableObjecSql> =
| DurableObjectState | DurableObjectState
| { | {
sql: DB; sql: DB;
}; };
export function doSqlite<DB extends DurableObjecSql>(config: DoConnectionConfig<DB>) { export function doSqlite<DB extends DurableObjecSql>(config: D1ConnectionConfig<DB>) {
const db = "sql" in config ? config.sql : config.storage.sql; const db = "sql" in config ? config.sql : config.storage.sql;
return genericSqlite( return genericSqlite(
@@ -21,7 +21,7 @@ export function doSqlite<DB extends DurableObjecSql>(config: DoConnectionConfig<
(utils) => { (utils) => {
// must be async to work with the miniflare mock // must be async to work with the miniflare mock
const getStmt = async (sql: string, parameters?: any[] | readonly any[]) => const getStmt = async (sql: string, parameters?: any[] | readonly any[]) =>
db.exec(sql, ...(parameters || [])); await db.exec(sql, ...(parameters || []));
const mapResult = ( const mapResult = (
cursor: SqlStorageCursor<Record<string, SqlStorageValue>>, cursor: SqlStorageCursor<Record<string, SqlStorageValue>>,

Some files were not shown because too many files have changed in this diff Show More