mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 16:16:02 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a046fa18e | |||
| 786d1d1bd4 | |||
| c384bf4dd4 | |||
| db2a994a01 | |||
| 166ea4a71b | |||
| 0d3bb3b7d6 | |||
| cfbec5b6ea | |||
| 5143ee5726 | |||
| fe1716ed01 | |||
| 0c31dcdb95 | |||
| 4cc0f8e172 | |||
| c161a26ec0 | |||
| 6e78a4c238 | |||
| 42edce904f |
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"bknd": {
|
||||
"url": "http://localhost:28623/api/system/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: "1.2.19"
|
||||
bun-version: "1.2.14"
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./app
|
||||
|
||||
+1
-3
@@ -31,6 +31,4 @@ packages/media/.env
|
||||
.git_old
|
||||
docker/tmp
|
||||
.debug
|
||||
.history
|
||||
.aider*
|
||||
.vercel
|
||||
.history
|
||||
@@ -1,9 +1,9 @@
|
||||
[](https://npmjs.org/package/bknd)
|
||||
|
||||

|
||||

|
||||
|
||||
<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>
|
||||
</a>
|
||||
</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.**
|
||||
|
||||
> [!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
|
||||
> 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
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
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)
|
||||
* 📦 Official API and React SDK with type-safety
|
||||
* ⚛️ React elements for auto-configured authentication and media components
|
||||
* 🛠️ MCP server, client and UI built-in to control your backend
|
||||
|
||||
## Structure
|
||||
The package is mainly split into 4 parts, each serving a specific purpose:
|
||||
|
||||
@@ -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 { getDummyConnection } from "./helper";
|
||||
import { Hono } from "hono";
|
||||
import * as proto from "../src/data/prototype";
|
||||
import { pick } from "lodash-es";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(() => disableConsoleLog());
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
afterEach(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||
afterEach(afterAllCleanup);
|
||||
|
||||
describe("App tests", async () => {
|
||||
test("boots and pongs", async () => {
|
||||
@@ -22,7 +19,7 @@ describe("App tests", async () => {
|
||||
test("plugins", async () => {
|
||||
const called: string[] = [];
|
||||
const app = createApp({
|
||||
config: {
|
||||
initialConfig: {
|
||||
auth: {
|
||||
enabled: true,
|
||||
},
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
tmp/*
|
||||
!tmp/.gitkeep
|
||||
tmp/*
|
||||
@@ -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 { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||
@@ -9,49 +9,60 @@ beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("adapter", () => {
|
||||
it("makes config", async () => {
|
||||
expect(omitKeys(await adapter.makeConfig({}), ["connection"])).toEqual({});
|
||||
expect(
|
||||
omitKeys(await adapter.makeConfig({}, { env: { TEST: "test" } }), ["connection"]),
|
||||
).toEqual({});
|
||||
it("makes config", () => {
|
||||
expect(omitKeys(adapter.makeConfig({}), ["connection"])).toEqual({});
|
||||
expect(omitKeys(adapter.makeConfig({}, { env: { TEST: "test" } }), ["connection"])).toEqual(
|
||||
{},
|
||||
);
|
||||
|
||||
// merges everything returned from `app` with the config
|
||||
expect(
|
||||
omitKeys(
|
||||
await adapter.makeConfig(
|
||||
{ app: (a) => ({ config: { server: { cors: { origin: a.env.TEST } } } }) },
|
||||
adapter.makeConfig(
|
||||
{ app: (a) => ({ initialConfig: { server: { cors: { origin: a.env.TEST } } } }) },
|
||||
{ env: { TEST: "test" } },
|
||||
),
|
||||
["connection"],
|
||||
),
|
||||
).toEqual({
|
||||
config: { server: { cors: { origin: "test" } } },
|
||||
initialConfig: { server: { cors: { origin: "test" } } },
|
||||
});
|
||||
});
|
||||
|
||||
it("allows all properties in app function", async () => {
|
||||
const called = mock(() => null);
|
||||
const config = await adapter.makeConfig(
|
||||
/* it.only("...", async () => {
|
||||
const app = await adapter.createAdapterApp();
|
||||
}); */
|
||||
|
||||
it("reuses apps correctly", async () => {
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
const first = await adapter.createAdapterApp(
|
||||
{
|
||||
app: (env) => ({
|
||||
connection: { url: "test" },
|
||||
config: { server: { cors: { origin: "test" } } },
|
||||
options: {
|
||||
mode: "db",
|
||||
},
|
||||
onBuilt: () => {
|
||||
called();
|
||||
expect(env).toEqual({ foo: "bar" });
|
||||
},
|
||||
}),
|
||||
initialConfig: { server: { cors: { origin: "random" } } },
|
||||
},
|
||||
{ foo: "bar" },
|
||||
undefined,
|
||||
{ id },
|
||||
);
|
||||
expect(config.connection).toEqual({ url: "test" });
|
||||
expect(config.config).toEqual({ server: { cors: { origin: "test" } } });
|
||||
expect(config.options).toEqual({ mode: "db" });
|
||||
await config.onBuilt?.(null as any);
|
||||
expect(called).toHaveBeenCalled();
|
||||
const second = await adapter.createAdapterApp();
|
||||
const third = await adapter.createAdapterApp(undefined, undefined, { id });
|
||||
|
||||
await first.build();
|
||||
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, {
|
||||
|
||||
@@ -42,6 +42,7 @@ describe("Api", async () => {
|
||||
expect(api.isAuthVerified()).toBe(false);
|
||||
|
||||
const params = api.getParams();
|
||||
console.log(params);
|
||||
expect(params.token).toBe(token);
|
||||
expect(params.token_transport).toBe("cookie");
|
||||
expect(params.host).toBe("http://example.com");
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
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 { DataController } from "../../src/data/api/DataController";
|
||||
import { dataConfigSchema } from "../../src/data/data-schema";
|
||||
import * as proto from "../../src/data/prototype";
|
||||
import { schemaToEm } from "../helper";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
import { parse } from "core/utils/schema";
|
||||
import { parse } from "core/object/schema";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
@@ -202,7 +202,7 @@ describe("DataApi", () => {
|
||||
{
|
||||
// create many
|
||||
const res = await api.createMany("posts", payload);
|
||||
expect(res.data?.length).toEqual(4);
|
||||
expect(res.data.length).toEqual(4);
|
||||
expect(res.ok).toBeTrue();
|
||||
}
|
||||
|
||||
|
||||
@@ -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 { App, createApp } from "core/test/utils";
|
||||
import * as proto from "data/prototype";
|
||||
import { DbModuleManager } from "modules/db/DbModuleManager";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
import * as proto from "../../src/data/prototype";
|
||||
|
||||
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 () => {
|
||||
const called = mock(() => null);
|
||||
await createApp({
|
||||
@@ -34,7 +20,6 @@ describe("App", () => {
|
||||
"guard",
|
||||
"flags",
|
||||
"logger",
|
||||
"mcp",
|
||||
"helper",
|
||||
]);
|
||||
},
|
||||
@@ -43,7 +28,7 @@ describe("App", () => {
|
||||
expect(called).toHaveBeenCalled();
|
||||
|
||||
const app = createApp({
|
||||
config: {
|
||||
initialConfig: {
|
||||
data: proto
|
||||
.em({
|
||||
todos: proto.entity("todos", {
|
||||
@@ -150,21 +135,4 @@ describe("App", () => {
|
||||
// expect async listeners to be executed sync after request
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,15 +8,10 @@ describe("AppServer", () => {
|
||||
expect(server).toBeDefined();
|
||||
expect(server.config).toEqual({
|
||||
cors: {
|
||||
allow_credentials: true,
|
||||
origin: "*",
|
||||
allow_methods: ["GET", "POST", "PATCH", "PUT", "DELETE"],
|
||||
allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"],
|
||||
},
|
||||
mcp: {
|
||||
enabled: false,
|
||||
path: "/api/system/mcp",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -30,15 +25,10 @@ describe("AppServer", () => {
|
||||
expect(server).toBeDefined();
|
||||
expect(server.config).toEqual({
|
||||
cors: {
|
||||
allow_credentials: true,
|
||||
origin: "https",
|
||||
allow_methods: ["GET", "POST"],
|
||||
allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"],
|
||||
},
|
||||
mcp: {
|
||||
enabled: false,
|
||||
path: "/api/system/mcp",
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,226 +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();
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,39 +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(app.mcp?.tools.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -1,346 +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();
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -1,118 +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();
|
||||
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");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,76 +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();
|
||||
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");
|
||||
});
|
||||
});
|
||||
@@ -1,56 +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();
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,7 @@
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { registries } from "../../src";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { createApp } from "core/test/utils";
|
||||
import * as proto from "../../src/data/prototype";
|
||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(() => disableConsoleLog());
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("repros", async () => {
|
||||
/**
|
||||
@@ -18,8 +13,8 @@ describe("repros", async () => {
|
||||
* There was an issue that AppData had old configs because of system entity "media"
|
||||
*/
|
||||
test("registers media entity correctly to relate to it", async () => {
|
||||
registries.media.register("local", StorageLocalAdapter);
|
||||
const app = createApp();
|
||||
app.module.media.adapters.set("local", StorageLocalAdapter);
|
||||
await app.build();
|
||||
|
||||
{
|
||||
@@ -92,7 +87,7 @@ describe("repros", async () => {
|
||||
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();
|
||||
|
||||
const info = (await (await app.server.request("/api/data/info/products")).json()) as any;
|
||||
|
||||
@@ -1,3 +1,45 @@
|
||||
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 { Guard } from "../../../src/auth/authorize/Guard";
|
||||
import { Guard } from "../../../src/auth";
|
||||
|
||||
describe("authorize", () => {
|
||||
test("basic", async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
|
||||
import { Event, EventManager, InvalidEventReturn, NoParamEvent } from "../../src/core/events";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
import { disableConsoleLog, enableConsoleLog } from "../helper";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Registry } from "core/registry/Registry";
|
||||
import { s } from "core/utils/schema";
|
||||
import { Registry } from "core";
|
||||
import { s } from "core/object/schema";
|
||||
|
||||
type Constructor<T> = new (...args: any[]) => T;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { s } from "core/utils/schema";
|
||||
import { SchemaObject } from "core/object/SchemaObject";
|
||||
import { SchemaObject } from "../../../src/core";
|
||||
import { s } from "core/object/schema";
|
||||
|
||||
describe("SchemaObject", async () => {
|
||||
test("basic", async () => {
|
||||
|
||||
@@ -248,7 +248,7 @@ describe("Core Utils", async () => {
|
||||
expect(utils.getContentName(request)).toBe(name);
|
||||
});
|
||||
|
||||
test("detectImageDimensions", async () => {
|
||||
test.only("detectImageDimensions", async () => {
|
||||
// wrong
|
||||
// @ts-expect-error
|
||||
expect(utils.detectImageDimensions(new ArrayBuffer(), "text/plain")).rejects.toThrow();
|
||||
@@ -267,12 +267,12 @@ describe("Core Utils", async () => {
|
||||
});
|
||||
|
||||
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");
|
||||
/*console.log(utils.datetimeStringUTC(new Date()));
|
||||
console.log(utils.datetimeStringUTC(new Date()));
|
||||
console.log(utils.datetimeStringUTC());
|
||||
console.log(new Date());
|
||||
console.log("timezone", Intl.DateTimeFormat().resolvedOptions().timeZone); */
|
||||
console.log("timezone", Intl.DateTimeFormat().resolvedOptions().timeZone);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
|
||||
import { Guard } from "../../src/auth/authorize/Guard";
|
||||
import { parse } from "core/utils/schema";
|
||||
|
||||
import { Guard } from "../../src/auth";
|
||||
import { parse } from "core/object/schema";
|
||||
import {
|
||||
Entity,
|
||||
type EntityData,
|
||||
EntityManager,
|
||||
ManyToOneRelation,
|
||||
TextField,
|
||||
} from "../../src/data";
|
||||
import { DataController } from "../../src/data/api/DataController";
|
||||
import { dataConfigSchema } from "../../src/data/data-schema";
|
||||
import { getDummyConnection } from "../helper";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
import { disableConsoleLog, enableConsoleLog, getDummyConnection } from "../helper";
|
||||
import type { RepositoryResultJSON } from "data/entities/query/RepositoryResult";
|
||||
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();
|
||||
beforeAll(() => disableConsoleLog());
|
||||
beforeAll(() => disableConsoleLog(["log", "warn"]));
|
||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||
|
||||
const dataConfig = parse(dataConfigSchema, {});
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { TextField, PrimaryField, NumberField } from "data/fields";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
NumberField,
|
||||
PrimaryField,
|
||||
Repository,
|
||||
TextField,
|
||||
} from "../../src/data";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
|
||||
@@ -2,7 +2,7 @@ import { unlink } from "node:fs/promises";
|
||||
import type { SqliteDatabase } from "kysely";
|
||||
// @ts-ignore
|
||||
import Database from "libsql";
|
||||
import { SqliteLocalConnection } from "data/connection/sqlite/SqliteLocalConnection";
|
||||
import { SqliteLocalConnection } from "../../src/data";
|
||||
|
||||
export function getDummyDatabase(memory: boolean = true): {
|
||||
dummyDb: SqliteDatabase;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { Entity } from "data/entities";
|
||||
import { EntityManager } from "data/entities/EntityManager";
|
||||
import { ManyToOneRelation } from "data/relations";
|
||||
import { NumberField, TextField } from "data/fields";
|
||||
import { SchemaManager } from "data/schema/SchemaManager";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
ManyToOneRelation,
|
||||
NumberField,
|
||||
SchemaManager,
|
||||
TextField,
|
||||
} from "../../src/data";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { NumberField, TextField } from "data/fields";
|
||||
import { TransformPersistFailedException } from "data/errors";
|
||||
import { Entity, EntityManager, Mutator, NumberField, TextField } from "../../src/data";
|
||||
import { TransformPersistFailedException } from "../../src/data/errors";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { afterAll, expect as bunExpect, describe, test } from "bun:test";
|
||||
import { stripMark } from "core/utils/schema";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { TextField } from "data/fields";
|
||||
import { PolymorphicRelation } from "data/relations";
|
||||
import { stripMark } from "core/object/schema";
|
||||
import { Entity, EntityManager, PolymorphicRelation, TextField } from "../../src/data";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
|
||||
@@ -2,20 +2,19 @@ import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
BooleanField,
|
||||
DateField,
|
||||
Entity,
|
||||
EntityIndex,
|
||||
EntityManager,
|
||||
EnumField,
|
||||
JsonField,
|
||||
NumberField,
|
||||
TextField,
|
||||
EntityIndex,
|
||||
} from "data/fields";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import {
|
||||
ManyToManyRelation,
|
||||
ManyToOneRelation,
|
||||
NumberField,
|
||||
OneToOneRelation,
|
||||
PolymorphicRelation,
|
||||
} from "data/relations";
|
||||
import { DummyConnection } from "data/connection/DummyConnection";
|
||||
TextField,
|
||||
} from "../../src/data";
|
||||
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
||||
import {
|
||||
FieldPrototype,
|
||||
type FieldSchema,
|
||||
@@ -33,8 +32,8 @@ import {
|
||||
number,
|
||||
relation,
|
||||
text,
|
||||
} from "data/prototype";
|
||||
import { MediaField } from "media/MediaField";
|
||||
} from "../../src/data/prototype";
|
||||
import { MediaField } from "../../src/media/MediaField";
|
||||
|
||||
describe("prototype", () => {
|
||||
test("...", () => {
|
||||
@@ -297,9 +296,9 @@ describe("prototype", () => {
|
||||
new Entity("posts", [new TextField("name"), new TextField("slug", { required: true })]),
|
||||
new Entity("comments", [new TextField("some")]),
|
||||
new Entity("users", [new TextField("email")]),
|
||||
] as const;
|
||||
];
|
||||
const _em2 = new EntityManager(
|
||||
[...es],
|
||||
es,
|
||||
new DummyConnection(),
|
||||
[new ManyToOneRelation(es[0], es[1]), new ManyToOneRelation(es[0], es[2])],
|
||||
[
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { TextField } from "data/fields";
|
||||
import { Entity, EntityManager, TextField } from "../../src/data";
|
||||
import {
|
||||
ManyToManyRelation,
|
||||
ManyToOneRelation,
|
||||
OneToOneRelation,
|
||||
PolymorphicRelation,
|
||||
RelationField,
|
||||
} from "data/relations";
|
||||
} from "../../src/data/relations";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
@@ -78,7 +77,7 @@ describe("Relations", async () => {
|
||||
const em = new EntityManager(entities, dummyConnection, relations);
|
||||
|
||||
// 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.reference).toBe(posts.name);
|
||||
expect(rel.target.entity.name).toBe(users.name);
|
||||
@@ -90,11 +89,11 @@ describe("Relations", async () => {
|
||||
// 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)[0]!.source.entity).toBe(posts);
|
||||
expect(em.relationsOf(users.name)[0].source.entity).toBe(posts);
|
||||
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)[0]!.source.entity).toBe(posts);
|
||||
expect(em.relationsOf(users.name)[0].source.entity).toBe(posts);
|
||||
|
||||
// verify high level relation (from users)
|
||||
const userPostsRel = em.relationOf(users.name, "posts");
|
||||
@@ -192,7 +191,7 @@ describe("Relations", async () => {
|
||||
const em = new EntityManager(entities, dummyConnection, relations);
|
||||
|
||||
// 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.reference).toBe(users.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)[0]!.source.entity).toBe(users);
|
||||
expect(em.relationsOf(users.name)[0]!.target.entity).toBe(settings);
|
||||
expect(em.relationsOf(users.name)[0].source.entity).toBe(users);
|
||||
expect(em.relationsOf(users.name)[0].target.entity).toBe(settings);
|
||||
|
||||
// verify high level relation (from users)
|
||||
const userSettingRel = em.relationOf(users.name, settings.name);
|
||||
@@ -324,7 +323,7 @@ describe("Relations", async () => {
|
||||
);
|
||||
|
||||
// mutation info
|
||||
expect(relations[0]!.helper(posts.name)!.getMutationInfo()).toEqual({
|
||||
expect(relations[0].helper(posts.name)!.getMutationInfo()).toEqual({
|
||||
reference: "categories",
|
||||
local_field: undefined,
|
||||
$set: false,
|
||||
@@ -335,7 +334,7 @@ describe("Relations", async () => {
|
||||
cardinality: undefined,
|
||||
relation_type: "m:n",
|
||||
});
|
||||
expect(relations[0]!.helper(categories.name)!.getMutationInfo()).toEqual({
|
||||
expect(relations[0].helper(categories.name)!.getMutationInfo()).toEqual({
|
||||
reference: "posts",
|
||||
local_field: undefined,
|
||||
$set: false,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Entity } from "data/entities";
|
||||
import { NumberField, TextField } from "data/fields";
|
||||
import { Entity, NumberField, TextField } from "data";
|
||||
import * as p from "data/prototype";
|
||||
|
||||
describe("[data] Entity", async () => {
|
||||
const entity = new Entity("test", [
|
||||
@@ -47,4 +47,8 @@ describe("[data] Entity", async () => {
|
||||
entity.addField(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 { Entity, EntityManager } from "data/entities";
|
||||
import { ManyToManyRelation, ManyToOneRelation } from "data/relations";
|
||||
import { SchemaManager } from "data/schema/SchemaManager";
|
||||
import { UnableToConnectException } from "data/errors";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
ManyToManyRelation,
|
||||
ManyToOneRelation,
|
||||
SchemaManager,
|
||||
} from "../../../src/data";
|
||||
import { UnableToConnectException } from "../../../src/data/errors";
|
||||
import { getDummyConnection } from "../helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { ManyToOneRelation } from "data/relations";
|
||||
import { TextField } from "data/fields";
|
||||
import { JoinBuilder } from "data/entities/query/JoinBuilder";
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { Entity, EntityManager, ManyToOneRelation, TextField } from "../../../src/data";
|
||||
import { JoinBuilder } from "../../../src/data/entities/query/JoinBuilder";
|
||||
import { getDummyConnection } from "../helper";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(() => disableConsoleLog());
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||
afterAll(afterAllCleanup);
|
||||
|
||||
describe("[data] JoinBuilder", async () => {
|
||||
test("missing relation", async () => {
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import type { EventManager } from "../../../src/core/events";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
ManyToOneRelation,
|
||||
MutatorEvents,
|
||||
NumberField,
|
||||
OneToOneRelation,
|
||||
RelationField,
|
||||
type RelationField,
|
||||
RelationMutator,
|
||||
} from "data/relations";
|
||||
import { NumberField, TextField } from "data/fields";
|
||||
import * as proto from "data/prototype";
|
||||
import { getDummyConnection } from "../../helper";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
import { MutatorEvents } from "data/events";
|
||||
TextField,
|
||||
} from "../../../src/data";
|
||||
import * as proto from "../../../src/data/prototype";
|
||||
import { getDummyConnection, disableConsoleLog, enableConsoleLog } from "../../helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
afterAll(afterAllCleanup);
|
||||
|
||||
beforeAll(() => disableConsoleLog());
|
||||
beforeAll(() => disableConsoleLog(["log", "warn"]));
|
||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||
|
||||
describe("[data] Mutator (base)", async () => {
|
||||
|
||||
@@ -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 { TextField } from "data/fields";
|
||||
import { em as $em, entity as $entity, text as $text } from "data/prototype";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { ManyToOneRelation } from "data/relations";
|
||||
import { RepositoryEvents } from "data/events";
|
||||
import { Perf } from "core/utils";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
LibsqlConnection,
|
||||
ManyToOneRelation,
|
||||
RepositoryEvents,
|
||||
TextField,
|
||||
entity as $entity,
|
||||
text as $text,
|
||||
em as $em,
|
||||
} from "data";
|
||||
import { getDummyConnection } from "../helper";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
type E = Kysely<any> | Transaction<any>;
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
beforeAll(() => disableConsoleLog());
|
||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||
afterAll(afterAllCleanup);
|
||||
|
||||
async function sleep(ms: number) {
|
||||
return new Promise((resolve) => {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { randomString } from "core/utils";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { TextField, EntityIndex } from "data/fields";
|
||||
import { SchemaManager } from "data/schema/SchemaManager";
|
||||
import { randomString } from "../../../src/core/utils";
|
||||
import { Entity, EntityIndex, EntityManager, SchemaManager, TextField } from "../../../src/data";
|
||||
import { getDummyConnection } from "../helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { getDummyConnection } from "../helper";
|
||||
import { WhereBuilder, type WhereQuery } from "data/entities/query/WhereBuilder";
|
||||
import { type WhereQuery, WhereBuilder } from "data";
|
||||
|
||||
function qb() {
|
||||
const c = getDummyConnection();
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { ManyToManyRelation, ManyToOneRelation, PolymorphicRelation } from "data/relations";
|
||||
import { TextField } from "data/fields";
|
||||
import * as proto from "data/prototype";
|
||||
import { WithBuilder } from "data/entities/query/WithBuilder";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
ManyToManyRelation,
|
||||
ManyToOneRelation,
|
||||
PolymorphicRelation,
|
||||
TextField,
|
||||
WithBuilder,
|
||||
} from "../../../src/data";
|
||||
import * as proto from "../../../src/data/prototype";
|
||||
import { schemaToEm } from "../../helper";
|
||||
import { getDummyConnection } from "../helper";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(() => disableConsoleLog());
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { EntityManager } from "data/entities/EntityManager";
|
||||
import { EntityManager } from "../../../../src/data";
|
||||
import { getDummyConnection } from "../../helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { bunTestRunner } from "adapter/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";
|
||||
|
||||
describe("[data] BooleanField", async () => {
|
||||
fieldTestSuite(bunTestRunner, BooleanField, { defaultValue: true, schemaType: "boolean" });
|
||||
fieldTestSuite({ expect, test }, BooleanField, { defaultValue: true, schemaType: "boolean" });
|
||||
|
||||
test("transformRetrieve", async () => {
|
||||
const field = new BooleanField("test");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, test } from "bun:test";
|
||||
import { DateField } from "data/fields";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { DateField, dateFieldConfigSchema } from "../../../../src/data";
|
||||
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { bunTestRunner } from "adapter/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";
|
||||
|
||||
function options(strings: string[]) {
|
||||
@@ -9,7 +8,7 @@ function options(strings: string[]) {
|
||||
|
||||
describe("[data] EnumField", async () => {
|
||||
fieldTestSuite(
|
||||
bunTestRunner,
|
||||
{ expect, test },
|
||||
// @ts-ignore
|
||||
EnumField,
|
||||
{ defaultValue: "a", schemaType: "text" },
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test";
|
||||
import { baseFieldConfigSchema, Field } from "../../../../src/data/fields/Field";
|
||||
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import { stripMark } from "core/utils/schema";
|
||||
import { stripMark } from "core/object/schema";
|
||||
|
||||
describe("[data] Field", async () => {
|
||||
class FieldSpec extends Field {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Entity } from "data/entities";
|
||||
import { Field, EntityIndex } from "data/fields";
|
||||
import { s } from "core/utils/schema";
|
||||
import { Entity, EntityIndex, Field } from "../../../../src/data";
|
||||
import { s } from "core/object/schema";
|
||||
|
||||
class TestField extends Field {
|
||||
protected getSchema(): any {
|
||||
@@ -23,4 +22,11 @@ describe("FieldIndex", async () => {
|
||||
expect(index.name).toEqual("idx_test_name");
|
||||
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 { JsonField } from "data/fields";
|
||||
import { JsonField } from "../../../../src/data";
|
||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||
|
||||
describe("[data] JsonField", async () => {
|
||||
const field = new JsonField("test");
|
||||
fieldTestSuite(bunTestRunner, JsonField, {
|
||||
fieldTestSuite({ expect, test }, JsonField, {
|
||||
defaultValue: { a: 1 },
|
||||
sampleValues: ["string", { test: 1 }, 1],
|
||||
schemaType: "text",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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";
|
||||
|
||||
describe("[data] JsonSchemaField", async () => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { bunTestRunner } from "adapter/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";
|
||||
|
||||
describe("[data] NumberField", async () => {
|
||||
@@ -16,5 +15,5 @@ describe("[data] NumberField", async () => {
|
||||
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 { PrimaryField } from "data/fields";
|
||||
import { PrimaryField } from "../../../../src/data";
|
||||
|
||||
describe("[data] PrimaryField", async () => {
|
||||
const field = new PrimaryField("primary");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { bunTestRunner } from "adapter/bun/test";
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { describe, expect, it, test } from "bun:test";
|
||||
import { Entity, type EntityManager } from "data/entities";
|
||||
import { Entity, type EntityManager } from "../../../../src/data";
|
||||
import {
|
||||
type BaseRelationConfig,
|
||||
EntityRelation,
|
||||
EntityRelationAnchor,
|
||||
ManyToManyRelation,
|
||||
RelationTypes,
|
||||
} from "data/relations";
|
||||
import * as proto from "data/prototype";
|
||||
} from "../../../../src/data/relations";
|
||||
|
||||
class TestEntityRelation extends EntityRelation {
|
||||
constructor(config?: BaseRelationConfig) {
|
||||
@@ -26,11 +24,11 @@ class TestEntityRelation extends EntityRelation {
|
||||
return this;
|
||||
}
|
||||
|
||||
buildWith(): any {
|
||||
buildWith(a: any, b: any, c: any): any {
|
||||
return;
|
||||
}
|
||||
|
||||
buildJoin(): any {
|
||||
buildJoin(a: any, b: any): any {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -77,15 +75,4 @@ describe("[data] EntityRelation", async () => {
|
||||
const relation2 = new TestEntityRelation({ required: 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,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
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<
|
||||
typeof StringifyTask.schema,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Task } from "../../src/flows";
|
||||
import { dynamic } from "../../src/flows/tasks/Task";
|
||||
import { s } from "core/utils/schema";
|
||||
import { s } from "core/object/schema";
|
||||
|
||||
describe.skip("Task", async () => {
|
||||
test("resolveParams: template with parse", async () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Hono } from "hono";
|
||||
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 { dynamic } from "../../src/flows/tasks/Task";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { isEqual } from "lodash-es";
|
||||
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";
|
||||
|
||||
/*beforeAll(disableConsoleLog);
|
||||
|
||||
+23
-4
@@ -2,12 +2,11 @@ import { unlink } from "node:fs/promises";
|
||||
import type { SelectQueryBuilder, SqliteDatabase } from "kysely";
|
||||
import Database from "libsql";
|
||||
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 { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { slugify } from "bknd/utils";
|
||||
import { type Connection, SqliteLocalConnection } from "data/connection";
|
||||
import { EntityManager } from "data/entities/EntityManager";
|
||||
import { slugify } from "core/utils/strings";
|
||||
|
||||
export function getDummyDatabase(memory: boolean = true): {
|
||||
dummyDb: SqliteDatabase;
|
||||
@@ -39,6 +38,26 @@ export function getLocalLibsqlConnection() {
|
||||
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>) {
|
||||
const { sql, parameters } = qb.compile();
|
||||
return { sql, parameters };
|
||||
@@ -46,7 +65,7 @@ export function compileQb(qb: SelectQueryBuilder<any, any, any>) {
|
||||
|
||||
export function prettyPrintQb(qb: SelectQueryBuilder<any, any, any>) {
|
||||
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> {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||
import { App, createApp, type AuthResponse } from "../../src";
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from "bun:test";
|
||||
import { App, createApp } from "../../src";
|
||||
import type { AuthResponse } from "../../src/auth";
|
||||
import { auth } from "../../src/auth/middlewares";
|
||||
import { randomString, secureRandomString, withDisabledConsole } from "../../src/core/utils";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
import { getDummyConnection } from "../helper";
|
||||
import { disableConsoleLog, enableConsoleLog, getDummyConnection } from "../helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
afterEach(afterAllCleanup);
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
@@ -63,10 +66,9 @@ const configs = {
|
||||
};
|
||||
|
||||
function createAuthApp() {
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
const app = createApp({
|
||||
connection: dummyConnection,
|
||||
config: {
|
||||
initialConfig: {
|
||||
auth: configs.auth,
|
||||
},
|
||||
});
|
||||
@@ -149,8 +151,8 @@ describe("integration auth", () => {
|
||||
|
||||
const { data: users } = await app.em.repository("users").findMany();
|
||||
expect(users.length).toBe(2);
|
||||
expect(users[0]?.email).toBe(configs.users.normal.email);
|
||||
expect(users[1]?.email).toBe(configs.users.admin.email);
|
||||
expect(users[0].email).toBe(configs.users.normal.email);
|
||||
expect(users[1].email).toBe(configs.users.admin.email);
|
||||
});
|
||||
|
||||
it("should log you in with API", async () => {
|
||||
@@ -221,7 +223,7 @@ describe("integration auth", () => {
|
||||
|
||||
app.server.get("/get", auth(), async (c) => {
|
||||
return c.json({
|
||||
user: c.get("auth")?.user ?? null,
|
||||
user: c.get("auth").user ?? null,
|
||||
});
|
||||
});
|
||||
app.server.get("/wait", auth(), async (c) => {
|
||||
@@ -240,7 +242,7 @@ describe("integration auth", () => {
|
||||
{
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
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(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 { Api } from "../../src/Api";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("integration config", () => {
|
||||
it("should create an entity", async () => {
|
||||
|
||||
@@ -1,25 +1,17 @@
|
||||
/// <reference types="@types/bun" />
|
||||
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { registries } from "../../src";
|
||||
import { createApp } from "core/test/utils";
|
||||
import { mergeObject, randomString } from "../../src/core/utils";
|
||||
import type { TAppMediaConfig } from "../../src/media/media-schema";
|
||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||
import { assetsPath, assetsTmpPath } from "../helper";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(() => {
|
||||
disableConsoleLog();
|
||||
registries.media.register("local", StorageLocalAdapter);
|
||||
});
|
||||
afterAll(enableConsoleLog);
|
||||
import { assetsPath, assetsTmpPath, disableConsoleLog, enableConsoleLog } from "../helper";
|
||||
|
||||
const path = `${assetsPath}/image.png`;
|
||||
|
||||
async function makeApp(mediaOverride: Partial<TAppMediaConfig> = {}) {
|
||||
const app = createApp({
|
||||
config: {
|
||||
initialConfig: {
|
||||
media: mergeObject(
|
||||
{
|
||||
enabled: true,
|
||||
@@ -35,6 +27,8 @@ async function makeApp(mediaOverride: Partial<TAppMediaConfig> = {}) {
|
||||
},
|
||||
});
|
||||
|
||||
app.module.media.adapters.set("local", StorageLocalAdapter);
|
||||
|
||||
await app.build();
|
||||
return app;
|
||||
}
|
||||
@@ -43,6 +37,9 @@ function makeName(ext: string) {
|
||||
return randomString(10) + "." + ext;
|
||||
}
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("MediaController", () => {
|
||||
test("accepts direct", async () => {
|
||||
const app = await makeApp();
|
||||
@@ -54,6 +51,7 @@ describe("MediaController", () => {
|
||||
body: file,
|
||||
});
|
||||
const result = (await res.json()) as any;
|
||||
console.log(result);
|
||||
expect(result.name).toBe(name);
|
||||
|
||||
const destFile = Bun.file(assetsTmpPath + "/" + name);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { type FileBody, Storage } from "../../src/media/storage/Storage";
|
||||
import * as StorageEvents from "../../src/media/storage/events";
|
||||
import { StorageAdapter } from "media/storage/StorageAdapter";
|
||||
import { StorageAdapter } from "media";
|
||||
|
||||
class TestAdapter extends StorageAdapter {
|
||||
files: Record<string, FileBody> = {};
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
||||
import { createApp } from "core/test/utils";
|
||||
import { AuthController } from "../../src/auth/api/AuthController";
|
||||
import { em, entity, make, text } from "data/prototype";
|
||||
import { AppAuth, type ModuleBuildContext } from "modules";
|
||||
import { em, entity, make, text } from "../../src/data";
|
||||
import { AppAuth, type ModuleBuildContext } from "../../src/modules";
|
||||
import { disableConsoleLog, enableConsoleLog } from "../helper";
|
||||
// @ts-ignore
|
||||
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("AppAuth", () => {
|
||||
test.skip("...", () => {
|
||||
test.only("...", () => {
|
||||
const auth = new AppAuth({});
|
||||
console.log(auth.toJSON());
|
||||
console.log(auth.config);
|
||||
@@ -150,7 +148,7 @@ describe("AppAuth", () => {
|
||||
|
||||
test("registers auth middleware for bknd routes only", async () => {
|
||||
const app = createApp({
|
||||
config: {
|
||||
initialConfig: {
|
||||
auth: {
|
||||
enabled: true,
|
||||
jwt: {
|
||||
@@ -180,7 +178,7 @@ describe("AppAuth", () => {
|
||||
|
||||
test("should allow additional user fields", async () => {
|
||||
const app = createApp({
|
||||
config: {
|
||||
initialConfig: {
|
||||
auth: {
|
||||
entity_name: "users",
|
||||
enabled: true,
|
||||
@@ -204,7 +202,7 @@ describe("AppAuth", () => {
|
||||
|
||||
test("ensure user field configs is always correct", async () => {
|
||||
const app = createApp({
|
||||
config: {
|
||||
initialConfig: {
|
||||
auth: {
|
||||
enabled: true,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { AppData, type ModuleBuildContext } from "../../src/modules";
|
||||
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
||||
|
||||
@@ -1,24 +1,18 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { createApp } from "core/test/utils";
|
||||
import { em, entity, text } from "data/prototype";
|
||||
import { registries } from "modules/registries";
|
||||
import { em, entity, text } from "../../src/data";
|
||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||
import { AppMedia } from "../../src/media/AppMedia";
|
||||
import { moduleTestSuite } from "./module-test-suite";
|
||||
|
||||
describe("AppMedia", () => {
|
||||
test.skip("...", () => {
|
||||
const media = new AppMedia();
|
||||
console.log(media.toJSON());
|
||||
});
|
||||
|
||||
moduleTestSuite(AppMedia);
|
||||
|
||||
test("should allow additional fields", async () => {
|
||||
registries.media.register("local", StorageLocalAdapter);
|
||||
//registries.media.register("local", StorageLocalAdapter);
|
||||
|
||||
const app = createApp({
|
||||
config: {
|
||||
initialConfig: {
|
||||
media: {
|
||||
entity_name: "media",
|
||||
enabled: true,
|
||||
@@ -36,6 +30,7 @@ describe("AppMedia", () => {
|
||||
}).toJSON(),
|
||||
},
|
||||
});
|
||||
app.module.media.adapters.set("local", StorageLocalAdapter);
|
||||
|
||||
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" });
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { s, stripMark } from "core/utils/schema";
|
||||
import { em, entity, index, text } from "data/prototype";
|
||||
import { EntityManager } from "data/entities/EntityManager";
|
||||
import { s, stripMark } from "core/object/schema";
|
||||
import { EntityManager, em, entity, index, text } from "../../src/data";
|
||||
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
||||
import { Module } from "../../src/modules/Module";
|
||||
import { ModuleHelper } from "modules/ModuleHelper";
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||
import { Connection, entity, text } from "data";
|
||||
import { Module } from "modules/Module";
|
||||
import { getDefaultConfig } from "modules/ModuleManager";
|
||||
import { type ConfigTable, DbModuleManager as ModuleManager } from "modules/db/DbModuleManager";
|
||||
|
||||
import { CURRENT_VERSION, TABLE_NAME } from "modules/db/migrations";
|
||||
import { type ConfigTable, getDefaultConfig, ModuleManager } from "modules/ModuleManager";
|
||||
import { CURRENT_VERSION, TABLE_NAME } from "modules/migrations";
|
||||
import { getDummyConnection } from "../helper";
|
||||
import { s, stripMark } from "core/utils/schema";
|
||||
import { Connection } from "data/connection/Connection";
|
||||
import { entity, text } from "data/prototype";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
import { s, stripMark } from "core/object/schema";
|
||||
|
||||
describe("ModuleManager", async () => {
|
||||
test("s1: no config, no build", async () => {
|
||||
@@ -138,7 +131,7 @@ describe("ModuleManager", async () => {
|
||||
const db = c2.dummyConnection.kysely;
|
||||
|
||||
const mm2 = new ModuleManager(c2.dummyConnection, {
|
||||
initial: { version: version - 1, ...json } as any,
|
||||
initial: { version: version - 1, ...json },
|
||||
});
|
||||
await mm2.syncConfigTable();
|
||||
await db
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { type InitialModuleConfigs, createApp } from "../../../src";
|
||||
|
||||
import { type Kysely, sql } from "kysely";
|
||||
@@ -6,10 +6,6 @@ import { getDummyConnection } from "../../helper";
|
||||
import v7 from "./samples/v7.json";
|
||||
import v8 from "./samples/v8.json";
|
||||
import v8_2 from "./samples/v8-2.json";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(() => disableConsoleLog());
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
// app expects migratable config to be present in database
|
||||
async function createVersionedApp(config: InitialModuleConfigs | any) {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { beforeEach, describe, expect, it } from "bun:test";
|
||||
|
||||
import { Hono } from "hono";
|
||||
import { Guard } from "auth/authorize/Guard";
|
||||
import { EventManager } from "core/events";
|
||||
import { EntityManager } from "data/entities/EntityManager";
|
||||
import { Module, type ModuleBuildContext } from "modules/Module";
|
||||
import { Guard } from "../../src/auth";
|
||||
import { DebugLogger } from "../../src/core";
|
||||
import { EventManager } from "../../src/core/events";
|
||||
import { EntityManager } from "../../src/data";
|
||||
import { Module, type ModuleBuildContext } from "../../src/modules/Module";
|
||||
import { getDummyConnection } from "../helper";
|
||||
import { ModuleHelper } from "modules/ModuleHelper";
|
||||
import { DebugLogger, McpServer } from "bknd/utils";
|
||||
|
||||
export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildContext {
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
@@ -19,7 +19,6 @@ export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildCon
|
||||
guard: new Guard(),
|
||||
flags: Module.ctx_flags,
|
||||
logger: new DebugLogger(false),
|
||||
mcp: new McpServer(),
|
||||
...overrides,
|
||||
};
|
||||
return {
|
||||
|
||||
@@ -102,9 +102,7 @@ describe("json form", () => {
|
||||
] satisfies [string, Exclude<JSONSchema, boolean>, boolean][];
|
||||
|
||||
for (const [pointer, schema, output] of examples) {
|
||||
expect(utils.isRequired(new Draft2019(schema), pointer, schema), `${pointer} `).toBe(
|
||||
output,
|
||||
);
|
||||
expect(utils.isRequired(new Draft2019(schema), pointer, schema)).toBe(output);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+1
-25
@@ -1,29 +1,6 @@
|
||||
import pkg from "./package.json" with { type: "json" };
|
||||
import c from "picocolors";
|
||||
import { formatNumber } from "bknd/utils";
|
||||
import * as esbuild from "esbuild";
|
||||
|
||||
const deps = Object.keys(pkg.dependencies);
|
||||
const external = ["jsonv-ts/*", "wrangler", ...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);
|
||||
}
|
||||
import { formatNumber } from "core/utils";
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: ["./src/cli/index.ts"],
|
||||
@@ -31,7 +8,6 @@ const result = await Bun.build({
|
||||
outdir: "./dist/cli",
|
||||
env: "PUBLIC_*",
|
||||
minify: true,
|
||||
external,
|
||||
define: {
|
||||
__isDev: "0",
|
||||
__version: JSON.stringify(pkg.version),
|
||||
|
||||
+82
-97
@@ -1,7 +1,6 @@
|
||||
import { $ } from "bun";
|
||||
import * as tsup from "tsup";
|
||||
import pkg from "./package.json" with { type: "json" };
|
||||
import c from "picocolors";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const watch = args.includes("--watch");
|
||||
@@ -10,14 +9,6 @@ const types = args.includes("--types");
|
||||
const sourcemap = args.includes("--sourcemap");
|
||||
const clean = args.includes("--clean");
|
||||
|
||||
// silence tsup
|
||||
const oldConsole = {
|
||||
log: console.log,
|
||||
warn: console.warn,
|
||||
};
|
||||
console.log = () => {};
|
||||
console.warn = () => {};
|
||||
|
||||
const define = {
|
||||
__isDev: "0",
|
||||
__version: JSON.stringify(pkg.version),
|
||||
@@ -36,11 +27,11 @@ function buildTypes() {
|
||||
Bun.spawn(["bun", "build:types"], {
|
||||
stdout: "inherit",
|
||||
onExit: () => {
|
||||
oldConsole.log(c.cyan("[Types]"), c.green("built"));
|
||||
console.info("Types built");
|
||||
Bun.spawn(["bun", "tsc-alias"], {
|
||||
stdout: "inherit",
|
||||
onExit: () => {
|
||||
oldConsole.log(c.cyan("[Types]"), c.green("aliased"));
|
||||
console.info("Types aliased");
|
||||
types_running = false;
|
||||
},
|
||||
});
|
||||
@@ -48,10 +39,6 @@ function buildTypes() {
|
||||
});
|
||||
}
|
||||
|
||||
if (types && !watch) {
|
||||
buildTypes();
|
||||
}
|
||||
|
||||
let watcher_timeout: any;
|
||||
function delayTypes() {
|
||||
if (!watch || !types) return;
|
||||
@@ -61,44 +48,57 @@ function delayTypes() {
|
||||
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
|
||||
const external = [
|
||||
...dependencies,
|
||||
"bun:test",
|
||||
"node:test",
|
||||
"node:assert/strict",
|
||||
"@libsql/client",
|
||||
"bknd",
|
||||
/^bknd\/.*/,
|
||||
"jsonv-ts",
|
||||
/^jsonv-ts\/.*/,
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Building backend and general API
|
||||
*/
|
||||
async function buildApi() {
|
||||
banner("Building API");
|
||||
await tsup.build({
|
||||
minify,
|
||||
sourcemap,
|
||||
watch,
|
||||
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",
|
||||
external: [...external],
|
||||
metafile: true,
|
||||
target: "esnext",
|
||||
platform: "browser",
|
||||
format: ["esm"],
|
||||
splitting: false,
|
||||
treeshake: true,
|
||||
loader: {
|
||||
".svg": "dataurl",
|
||||
},
|
||||
onSuccess: async () => {
|
||||
delayTypes();
|
||||
oldConsole.log(c.cyan("[API]"), c.green("built"));
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -142,6 +142,7 @@ async function buildUi() {
|
||||
},
|
||||
} satisfies tsup.Options;
|
||||
|
||||
banner("Building UI");
|
||||
await tsup.build({
|
||||
...base,
|
||||
entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"],
|
||||
@@ -149,10 +150,10 @@ async function buildUi() {
|
||||
onSuccess: async () => {
|
||||
await rewriteClient("./dist/ui/index.js");
|
||||
delayTypes();
|
||||
oldConsole.log(c.cyan("[UI]"), c.green("built"));
|
||||
},
|
||||
});
|
||||
|
||||
banner("Building Client");
|
||||
await tsup.build({
|
||||
...base,
|
||||
entry: ["src/ui/client/index.ts"],
|
||||
@@ -160,7 +161,6 @@ async function buildUi() {
|
||||
onSuccess: async () => {
|
||||
await rewriteClient("./dist/ui/client/index.js");
|
||||
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"
|
||||
*/
|
||||
async function buildUiElements() {
|
||||
banner("Building UI Elements");
|
||||
await tsup.build({
|
||||
minify,
|
||||
sourcemap,
|
||||
@@ -204,7 +205,6 @@ async function buildUiElements() {
|
||||
onSuccess: async () => {
|
||||
await rewriteClient("./dist/ui/elements/index.js");
|
||||
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,
|
||||
onSuccess: async () => {
|
||||
delayTypes();
|
||||
oldConsole.log(c.cyan("[Adapter]"), adapter || "base", c.green("built"));
|
||||
},
|
||||
...overrides,
|
||||
define: {
|
||||
@@ -244,79 +243,65 @@ function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsu
|
||||
}
|
||||
|
||||
async function buildAdapters() {
|
||||
await Promise.all([
|
||||
// base adapter handles
|
||||
tsup.build({
|
||||
...baseConfig(""),
|
||||
target: "esnext",
|
||||
platform: "neutral",
|
||||
entry: ["src/adapter/index.ts"],
|
||||
outDir: "dist/adapter",
|
||||
// only way to keep @vite-ignore comments
|
||||
minify: false,
|
||||
}),
|
||||
banner("Building Adapters");
|
||||
// base adapter handles
|
||||
await tsup.build({
|
||||
...baseConfig(""),
|
||||
entry: ["src/adapter/index.ts"],
|
||||
outDir: "dist/adapter",
|
||||
});
|
||||
|
||||
// specific adatpers
|
||||
tsup.build(baseConfig("react-router")),
|
||||
tsup.build(
|
||||
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", {
|
||||
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,
|
||||
// specific adatpers
|
||||
await tsup.build(baseConfig("react-router"));
|
||||
await tsup.build(
|
||||
baseConfig("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
@@ -2,5 +2,4 @@
|
||||
#registry = "http://localhost:4873"
|
||||
|
||||
[test]
|
||||
coverageSkipTestFiles = true
|
||||
console.depth = 10
|
||||
coverageSkipTestFiles = true
|
||||
+6
-6
@@ -17,7 +17,7 @@ async function run(
|
||||
});
|
||||
|
||||
// Read from stdout
|
||||
const reader = (proc.stdout as ReadableStream).getReader();
|
||||
const reader = proc.stdout.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
// Function to read chunks
|
||||
@@ -30,7 +30,7 @@ async function run(
|
||||
|
||||
const text = decoder.decode(value);
|
||||
if (!resolveCalled) {
|
||||
console.info(c.dim(text.replace(/\n$/, "")));
|
||||
console.log(c.dim(text.replace(/\n$/, "")));
|
||||
}
|
||||
onChunk(
|
||||
text,
|
||||
@@ -189,21 +189,21 @@ const adapters = {
|
||||
|
||||
async function testAdapter(name: keyof typeof adapters) {
|
||||
const config = adapters[name];
|
||||
console.info("adapter", c.cyan(name));
|
||||
console.log("adapter", c.cyan(name));
|
||||
await config.clean();
|
||||
|
||||
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);
|
||||
|
||||
const add_env = "env" in config && config.env ? config.env : "";
|
||||
await $`TEST_URL=${data} TEST_ADAPTER=${name} ${add_env} bun run test:e2e`;
|
||||
console.info("DONE!");
|
||||
console.log("DONE!");
|
||||
|
||||
while (!proc.killed) {
|
||||
proc.kill("SIGINT");
|
||||
await Bun.sleep(250);
|
||||
console.info("Waiting for process to exit...");
|
||||
console.log("Waiting for process to exit...");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
+33
-25
@@ -3,7 +3,7 @@
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"bin": "./dist/cli/index.js",
|
||||
"version": "0.18.0-rc.4",
|
||||
"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.",
|
||||
"homepage": "https://bknd.io",
|
||||
"repository": {
|
||||
@@ -13,9 +13,8 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/bknd-io/bknd/issues"
|
||||
},
|
||||
"packageManager": "bun@1.2.19",
|
||||
"engines": {
|
||||
"node": ">=22.13"
|
||||
"node": ">=22"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "BKND_CLI_LOG_LEVEL=debug vite",
|
||||
@@ -30,7 +29,7 @@
|
||||
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly && tsc-alias",
|
||||
"updater": "bun x npm-check-updates -ui",
|
||||
"cli": "LOCAL=1 bun src/cli/index.ts",
|
||||
"prepublishOnly": "bun run types && bun run test && bun run test:node && 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",
|
||||
"test": "ALL_TESTS=1 bun test --bail",
|
||||
"test:all": "bun run test && bun run test:node",
|
||||
@@ -41,10 +40,9 @@
|
||||
"test:vitest:coverage": "vitest run --coverage",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:adapters": "bun run e2e/adapters.ts",
|
||||
"test:e2e:ui": "VITE_DB_URL=:memory: playwright test --ui",
|
||||
"test:e2e:debug": "VITE_DB_URL=:memory: playwright test --debug",
|
||||
"test:e2e:report": "VITE_DB_URL=:memory: playwright show-report",
|
||||
"docs:build-assets": "bun internal/docs.build-assets.ts"
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"test:e2e:debug": "playwright test --debug",
|
||||
"test:e2e:report": "playwright show-report"
|
||||
},
|
||||
"license": "FSL-1.1-MIT",
|
||||
"dependencies": {
|
||||
@@ -62,26 +60,24 @@
|
||||
"bcryptjs": "^3.0.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"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-to-ts": "^3.1.1",
|
||||
"jsonv-ts": "0.8.2",
|
||||
"kysely": "0.27.6",
|
||||
"kysely": "^0.27.6",
|
||||
"lodash-es": "^4.17.21",
|
||||
"oauth4webapi": "^2.11.1",
|
||||
"object-path-immutable": "^4.1.2",
|
||||
"radix-ui": "^1.1.3",
|
||||
"picocolors": "^1.1.1",
|
||||
"swr": "^2.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aws-sdk/client-s3": "^3.758.0",
|
||||
"@bluwy/giget-core": "^0.1.2",
|
||||
"@clack/prompts": "^0.11.0",
|
||||
"@cloudflare/vitest-pool-workers": "^0.9.3",
|
||||
"@cloudflare/vitest-pool-workers": "^0.8.38",
|
||||
"@cloudflare/workers-types": "^4.20250606.0",
|
||||
"@dagrejs/dagre": "^1.1.4",
|
||||
"@hono/vite-dev-server": "^0.21.0",
|
||||
"@hono/vite-dev-server": "^0.19.1",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@libsql/client": "^0.15.9",
|
||||
"@mantine/modals": "^7.17.1",
|
||||
@@ -104,11 +100,13 @@
|
||||
"dotenv": "^16.4.7",
|
||||
"jotai": "^2.12.2",
|
||||
"jsdom": "^26.0.0",
|
||||
"jsonv-ts": "^0.2.2",
|
||||
"kysely-d1": "^0.3.0",
|
||||
"kysely-generic-sqlite": "^1.2.1",
|
||||
"libsql-stateless-easy": "^1.8.0",
|
||||
"open": "^10.1.0",
|
||||
"openapi-types": "^12.1.3",
|
||||
"picocolors": "^1.1.1",
|
||||
"postcss": "^8.5.3",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
@@ -130,9 +128,7 @@
|
||||
"vite-plugin-circular-dependency": "^0.5.0",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.0.9",
|
||||
"wouter": "^3.6.0",
|
||||
"wrangler": "^4.37.1",
|
||||
"miniflare": "^4.20250913.0"
|
||||
"wouter": "^3.6.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@hono/node-server": "^1.14.3"
|
||||
@@ -165,6 +161,16 @@
|
||||
"import": "./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": {
|
||||
"types": "./dist/types/core/utils/index.d.ts",
|
||||
"import": "./dist/core/utils/index.js",
|
||||
@@ -175,6 +181,11 @@
|
||||
"import": "./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": {
|
||||
"types": "./dist/types/plugins/index.d.ts",
|
||||
"import": "./dist/plugins/index.js",
|
||||
@@ -199,11 +210,6 @@
|
||||
"import": "./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": {
|
||||
"types": "./dist/types/adapter/index.d.ts",
|
||||
"import": "./dist/adapter/index.js"
|
||||
@@ -245,13 +251,15 @@
|
||||
},
|
||||
"./dist/main.css": "./dist/ui/main.css",
|
||||
"./dist/styles.css": "./dist/ui/styles.css",
|
||||
"./dist/manifest.json": "./dist/static/.vite/manifest.json",
|
||||
"./static/*": "./dist/static/*"
|
||||
"./dist/manifest.json": "./dist/static/.vite/manifest.json"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"data": ["./dist/types/data/index.d.ts"],
|
||||
"core": ["./dist/types/core/index.d.ts"],
|
||||
"utils": ["./dist/types/core/utils/index.d.ts"],
|
||||
"cli": ["./dist/types/cli/index.d.ts"],
|
||||
"media": ["./dist/types/media/index.d.ts"],
|
||||
"plugins": ["./dist/types/plugins/index.d.ts"],
|
||||
"adapter": ["./dist/types/adapter/index.d.ts"],
|
||||
"adapter/cloudflare": ["./dist/types/adapter/cloudflare/index.d.ts"],
|
||||
|
||||
+3
-3
@@ -1,10 +1,10 @@
|
||||
import type { SafeUser } from "bknd";
|
||||
import type { SafeUser } from "auth";
|
||||
import { AuthApi, type AuthApiOptions } from "auth/api/AuthApi";
|
||||
import { DataApi, type DataApiOptions } from "data/api/DataApi";
|
||||
import { decode } from "hono/jwt";
|
||||
import { MediaApi, type MediaApiOptions } from "media/api/MediaApi";
|
||||
import { SystemApi } from "modules/SystemApi";
|
||||
import { omitKeys } from "bknd/utils";
|
||||
import { omitKeys } from "core/utils";
|
||||
import type { BaseModuleApiOptions } from "modules";
|
||||
|
||||
export type TApiUser = SafeUser;
|
||||
@@ -43,7 +43,7 @@ export type ApiOptions = {
|
||||
} & (
|
||||
| {
|
||||
token?: string;
|
||||
user?: TApiUser | null;
|
||||
user?: TApiUser;
|
||||
}
|
||||
| {
|
||||
request: Request;
|
||||
|
||||
+29
-79
@@ -1,22 +1,21 @@
|
||||
import type { CreateUserPayload } from "auth/AppAuth";
|
||||
import { $console, McpClient } from "bknd/utils";
|
||||
import { $console } from "core/utils";
|
||||
import { Event } from "core/events";
|
||||
import type { em as prototypeEm } from "data/prototype";
|
||||
import { Connection } from "data/connection/Connection";
|
||||
import type { Hono } from "hono";
|
||||
import {
|
||||
type InitialModuleConfigs,
|
||||
type ModuleConfigs,
|
||||
type Modules,
|
||||
ModuleManager,
|
||||
type InitialModuleConfigs,
|
||||
type ModuleBuildContext,
|
||||
type ModuleConfigs,
|
||||
type ModuleManagerOptions,
|
||||
type Modules,
|
||||
} from "modules/ModuleManager";
|
||||
import { DbModuleManager } from "modules/db/DbModuleManager";
|
||||
import * as SystemPermissions from "modules/permissions";
|
||||
import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
|
||||
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 { IEmailDriver, ICacheDriver } from "core/drivers";
|
||||
|
||||
@@ -24,34 +23,14 @@ import type { IEmailDriver, ICacheDriver } from "core/drivers";
|
||||
import { Api, type ApiOptions } from "Api";
|
||||
|
||||
export type AppPluginConfig = {
|
||||
/**
|
||||
* The name of the plugin.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The schema of the plugin.
|
||||
*/
|
||||
schema?: () => MaybePromise<ReturnType<typeof prototypeEm> | void>;
|
||||
/**
|
||||
* Called before the app is built.
|
||||
*/
|
||||
beforeBuild?: () => MaybePromise<void>;
|
||||
/**
|
||||
* Called after the app is built.
|
||||
*/
|
||||
onBuilt?: () => MaybePromise<void>;
|
||||
/**
|
||||
* Called when the server is initialized.
|
||||
*/
|
||||
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>;
|
||||
onBoot?: () => MaybePromise<void>;
|
||||
onModulesCreated?: (modules: Modules) => void;
|
||||
};
|
||||
export type AppPlugin = (app: App) => AppPluginConfig;
|
||||
|
||||
@@ -62,9 +41,6 @@ export class AppConfigUpdatedEvent extends AppEvent<{
|
||||
}> {
|
||||
static override slug = "app-config-updated";
|
||||
}
|
||||
/**
|
||||
* @type {Event<{ app: App }>}
|
||||
*/
|
||||
export class AppBuiltEvent extends AppEvent {
|
||||
static override slug = "app-built";
|
||||
}
|
||||
@@ -94,23 +70,17 @@ export type AppOptions = {
|
||||
email?: IEmailDriver;
|
||||
cache?: ICacheDriver;
|
||||
};
|
||||
mode?: "db" | "code";
|
||||
readonly?: boolean;
|
||||
};
|
||||
export type CreateAppConfig = {
|
||||
connection?: Connection | { url: string };
|
||||
config?: PartialRec<ModuleConfigs>;
|
||||
initialConfig?: InitialModuleConfigs;
|
||||
options?: AppOptions;
|
||||
};
|
||||
|
||||
export type AppConfig = { version: number } & ModuleConfigs;
|
||||
export type AppConfig = InitialModuleConfigs;
|
||||
export type LocalApiOptions = Request | ApiOptions;
|
||||
|
||||
export class App<
|
||||
C extends Connection = Connection,
|
||||
Config extends PartialRec<ModuleConfigs> = PartialRec<ModuleConfigs>,
|
||||
Options extends AppOptions = AppOptions,
|
||||
> {
|
||||
export class App<C extends Connection = Connection, Options extends AppOptions = AppOptions> {
|
||||
static readonly Events = AppEvents;
|
||||
|
||||
modules: ModuleManager;
|
||||
@@ -121,12 +91,11 @@ export class App<
|
||||
|
||||
private trigger_first_boot = false;
|
||||
private _building: boolean = false;
|
||||
private _systemController: SystemController | null = null;
|
||||
|
||||
constructor(
|
||||
public connection: C,
|
||||
_config?: Config,
|
||||
public options?: Options,
|
||||
_initialConfig?: InitialModuleConfigs,
|
||||
private options?: Options,
|
||||
) {
|
||||
this.drivers = options?.drivers ?? {};
|
||||
|
||||
@@ -138,29 +107,18 @@ export class App<
|
||||
this.plugins.set(config.name, config);
|
||||
}
|
||||
this.runPlugins("onBoot");
|
||||
|
||||
// use db manager by default
|
||||
const Manager = this.mode === "db" ? DbModuleManager : ModuleManager;
|
||||
|
||||
this.modules = new Manager(connection, {
|
||||
this.modules = new ModuleManager(connection, {
|
||||
...(options?.manager ?? {}),
|
||||
initial: _config,
|
||||
initial: _initialConfig,
|
||||
onUpdated: this.onUpdated.bind(this),
|
||||
onFirstBoot: this.onFirstBoot.bind(this),
|
||||
onServerInit: this.onServerInit.bind(this),
|
||||
onModulesBuilt: this.onModulesBuilt.bind(this),
|
||||
onModulesCreated: this.onModulesCreated.bind(this),
|
||||
});
|
||||
this.modules.ctx().emgr.registerEvents(AppEvents);
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this.options?.mode ?? "db";
|
||||
}
|
||||
|
||||
isReadOnly() {
|
||||
return Boolean(this.mode === "code" || this.options?.readonly);
|
||||
}
|
||||
|
||||
get emgr() {
|
||||
return this.modules.ctx().emgr;
|
||||
}
|
||||
@@ -191,7 +149,7 @@ export class App<
|
||||
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
|
||||
if (this._building) {
|
||||
while (this._building) {
|
||||
@@ -204,14 +162,13 @@ export class App<
|
||||
this._building = 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
|
||||
guard.registerPermissions(Object.values(SystemPermissions));
|
||||
this._systemController = new SystemController(this);
|
||||
this._systemController.register(this);
|
||||
server.route("/api/system", new SystemController(this).getController());
|
||||
|
||||
// emit built event
|
||||
$console.log("App built");
|
||||
@@ -231,6 +188,10 @@ export class App<
|
||||
this._building = false;
|
||||
}
|
||||
|
||||
mutateConfig<Module extends keyof Modules>(module: Module) {
|
||||
return this.modules.mutateConfigSafe(module);
|
||||
}
|
||||
|
||||
get server() {
|
||||
return this.modules.server;
|
||||
}
|
||||
@@ -239,10 +200,6 @@ export class App<
|
||||
return this.modules.ctx().em;
|
||||
}
|
||||
|
||||
get mcp() {
|
||||
return this._systemController?._mcpServer;
|
||||
}
|
||||
|
||||
get fetch(): Hono["fetch"] {
|
||||
return this.server.fetch as any;
|
||||
}
|
||||
@@ -301,18 +258,6 @@ export class App<
|
||||
return new Api({ host: "http://localhost", ...(options ?? {}), fetcher });
|
||||
}
|
||||
|
||||
getMcpClient() {
|
||||
if (!this.mcp) {
|
||||
throw new Error("MCP is not enabled");
|
||||
}
|
||||
const mcpPath = this.modules.get("server").config.mcp.path;
|
||||
|
||||
return new McpClient({
|
||||
url: "http://localhost" + mcpPath,
|
||||
fetch: this.server.request,
|
||||
});
|
||||
}
|
||||
|
||||
async onUpdated<Module extends keyof Modules>(module: Module, config: ModuleConfigs[Module]) {
|
||||
// if the EventManager was disabled, we assume we shouldn't
|
||||
// respond to events, such as "onUpdated".
|
||||
@@ -382,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 = {}) {
|
||||
@@ -389,5 +339,5 @@ export function createApp(config: CreateAppConfig = {}) {
|
||||
throw new Error("Invalid connection");
|
||||
}
|
||||
|
||||
return new App(config.connection, config.config, config.options);
|
||||
return new App(config.connection, config.initialConfig, config.options);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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 { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
export function adapterTestSuite<
|
||||
Config extends BkndConfig = BkndConfig,
|
||||
@@ -14,17 +13,24 @@ export function adapterTestSuite<
|
||||
label = "app",
|
||||
overrides = {},
|
||||
}: {
|
||||
makeApp: (config: Config, args?: Args) => Promise<App>;
|
||||
makeHandler?: (config?: Config, args?: Args) => (request: Request) => Promise<Response>;
|
||||
makeApp: (
|
||||
config: Config,
|
||||
args?: Args,
|
||||
opts?: RuntimeOptions | FrameworkOptions,
|
||||
) => Promise<App>;
|
||||
makeHandler?: (
|
||||
config?: Config,
|
||||
args?: Args,
|
||||
opts?: RuntimeOptions | FrameworkOptions,
|
||||
) => (request: Request) => Promise<Response>;
|
||||
label?: string;
|
||||
overrides?: {
|
||||
dbUrl?: string;
|
||||
};
|
||||
},
|
||||
) {
|
||||
const { test, expect, mock, beforeAll, afterAll } = testRunner;
|
||||
beforeAll(() => disableConsoleLog());
|
||||
afterAll(() => enableConsoleLog());
|
||||
const { test, expect, mock } = testRunner;
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
test(`creates ${label}`, async () => {
|
||||
const beforeBuild = mock(async () => null) as any;
|
||||
@@ -33,7 +39,7 @@ export function adapterTestSuite<
|
||||
const config = {
|
||||
app: (env) => ({
|
||||
connection: { url: env.url },
|
||||
config: {
|
||||
initialConfig: {
|
||||
server: { cors: { origin: env.origin } },
|
||||
},
|
||||
}),
|
||||
@@ -47,10 +53,11 @@ export function adapterTestSuite<
|
||||
url: overrides.dbUrl ?? ":memory:",
|
||||
origin: "localhost",
|
||||
} as any,
|
||||
{ id },
|
||||
);
|
||||
expect(app).toBeDefined();
|
||||
expect(app.toJSON().server.cors.origin).toEqual("localhost");
|
||||
expect(beforeBuild).toHaveBeenCalledTimes(2);
|
||||
expect(beforeBuild).toHaveBeenCalledTimes(1);
|
||||
expect(onBuilt).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -61,7 +68,7 @@ export function adapterTestSuite<
|
||||
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 { res, data } = await getConfig(fetcher);
|
||||
@@ -70,14 +77,14 @@ export function adapterTestSuite<
|
||||
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
|
||||
const fetcher = makeHandler(undefined, undefined, { id });
|
||||
const fetcher = makeHandler(undefined, undefined, { id, force: true });
|
||||
|
||||
const { res, data } = await getConfig(fetcher);
|
||||
expect(res.ok).toBe(true);
|
||||
expect(res.status).toBe(200);
|
||||
expect(data.server.cors.origin).toEqual("*");
|
||||
}); */
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,6 @@ afterAll(enableConsoleLog);
|
||||
describe("astro adapter", () => {
|
||||
adapterTestSuite(bunTestRunner, {
|
||||
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 }),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 TAstro = {
|
||||
@@ -9,12 +9,17 @@ export type AstroBkndConfig<Env = AstroEnv> = FrameworkBkndConfig<Env>;
|
||||
export async function getApp<Env = AstroEnv>(
|
||||
config: AstroBkndConfig<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 (await getApp(config, args)).fetch(fnArgs.request);
|
||||
return (await getApp(config, args, opts)).fetch(fnArgs.request);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { App } from "bknd";
|
||||
import { handle } from "hono/aws-lambda";
|
||||
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;
|
||||
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>(
|
||||
{ adminOptions = false, assets, ...config }: AwsLambdaBkndConfig<Env> = {},
|
||||
args: Env = {} as Env,
|
||||
opts?: RuntimeOptions,
|
||||
): Promise<App> {
|
||||
let additional: Partial<RuntimeBkndConfig> = {
|
||||
adminOptions,
|
||||
@@ -56,15 +57,17 @@ export async function createApp<Env extends AwsLambdaEnv = AwsLambdaEnv>(
|
||||
...additional,
|
||||
},
|
||||
args ?? process.env,
|
||||
opts,
|
||||
);
|
||||
}
|
||||
|
||||
export function serve<Env extends AwsLambdaEnv = AwsLambdaEnv>(
|
||||
config: AwsLambdaBkndConfig<Env> = {},
|
||||
args: Env = {} as Env,
|
||||
opts?: RuntimeOptions,
|
||||
) {
|
||||
return async (event) => {
|
||||
const app = await createApp(config, args);
|
||||
const app = await createApp(config, args, opts);
|
||||
return await handle(app.server)(event);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ describe("aws adapter", () => {
|
||||
adapterTestSuite(bunTestRunner, {
|
||||
makeApp: awsLambda.createApp,
|
||||
// @todo: add a request to lambda event translator?
|
||||
makeHandler: (c, a) => async (request: Request) => {
|
||||
const app = await awsLambda.createApp(c, a);
|
||||
makeHandler: (c, a, o) => async (request: Request) => {
|
||||
const app = await awsLambda.createApp(c, a, o);
|
||||
return app.fetch(request);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,43 +1,44 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
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 { config, type App } from "bknd";
|
||||
import { config } from "bknd/core";
|
||||
import type { ServeOptions } from "bun";
|
||||
import { serveStatic } from "hono/bun";
|
||||
import type { App } from "App";
|
||||
|
||||
type BunEnv = Bun.Env;
|
||||
export type BunBkndConfig<Env = BunEnv> = RuntimeBkndConfig<Env> & Omit<ServeOptions, "fetch">;
|
||||
|
||||
export async function createApp<Env = BunEnv>(
|
||||
{ distPath, serveStatic: _serveStatic, ...config }: BunBkndConfig<Env> = {},
|
||||
{ distPath, ...config }: BunBkndConfig<Env> = {},
|
||||
args: Env = {} as Env,
|
||||
opts?: RuntimeOptions,
|
||||
) {
|
||||
const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static");
|
||||
registerLocalMediaAdapter();
|
||||
|
||||
return await createRuntimeApp(
|
||||
const app = await createRuntimeApp(
|
||||
{
|
||||
serveStatic:
|
||||
_serveStatic ??
|
||||
serveStatic({
|
||||
root,
|
||||
}),
|
||||
...config,
|
||||
serveStatic: serveStatic({ root }),
|
||||
},
|
||||
args ?? (process.env as Env),
|
||||
opts,
|
||||
);
|
||||
registerLocalMediaAdapter(app);
|
||||
return app;
|
||||
}
|
||||
|
||||
export function createHandler<Env = BunEnv>(
|
||||
config: BunBkndConfig<Env> = {},
|
||||
args: Env = {} as Env,
|
||||
opts?: RuntimeOptions,
|
||||
) {
|
||||
let app: App | undefined;
|
||||
return async (req: Request) => {
|
||||
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);
|
||||
};
|
||||
@@ -47,16 +48,16 @@ export function serve<Env = BunEnv>(
|
||||
{
|
||||
distPath,
|
||||
connection,
|
||||
config: _config,
|
||||
initialConfig,
|
||||
options,
|
||||
port = config.server.default_port,
|
||||
onBuilt,
|
||||
buildConfig,
|
||||
adminOptions,
|
||||
serveStatic,
|
||||
...serveOptions
|
||||
}: BunBkndConfig<Env> = {},
|
||||
args: Env = {} as Env,
|
||||
opts?: RuntimeOptions,
|
||||
) {
|
||||
Bun.serve({
|
||||
...serveOptions,
|
||||
@@ -64,15 +65,15 @@ export function serve<Env = BunEnv>(
|
||||
fetch: createHandler(
|
||||
{
|
||||
connection,
|
||||
config: _config,
|
||||
initialConfig,
|
||||
options,
|
||||
onBuilt,
|
||||
buildConfig,
|
||||
adminOptions,
|
||||
distPath,
|
||||
serveStatic,
|
||||
},
|
||||
args,
|
||||
opts,
|
||||
),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { connectionTestSuite } from "data/connection/connection-test-suite";
|
||||
import { bunSqlite } from "./BunSqliteConnection";
|
||||
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 { GenericSqliteConnection } from "data/connection/sqlite/GenericSqliteConnection";
|
||||
|
||||
describe("BunSqliteConnection", () => {
|
||||
connectionTestSuite(bunTestRunner, {
|
||||
@@ -13,20 +12,4 @@ describe("BunSqliteConnection", () => {
|
||||
}),
|
||||
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 {
|
||||
genericSqlite,
|
||||
type GenericSqliteConnection,
|
||||
type GenericSqliteConnectionConfig,
|
||||
} from "bknd";
|
||||
import { omitKeys } from "bknd/utils";
|
||||
import { genericSqlite, type GenericSqliteConnection } from "bknd/data";
|
||||
|
||||
export type BunSqliteConnection = GenericSqliteConnection<Database>;
|
||||
export type BunSqliteConnectionConfig = Omit<
|
||||
GenericSqliteConnectionConfig<Database>,
|
||||
"name" | "supports"
|
||||
> &
|
||||
({ database?: Database; url?: never } | { url?: string; database?: never });
|
||||
export type BunSqliteConnectionConfig = {
|
||||
database: Database;
|
||||
};
|
||||
|
||||
export function bunSqlite(config?: BunSqliteConnectionConfig) {
|
||||
let db: Database | undefined;
|
||||
export function bunSqlite(config?: BunSqliteConnectionConfig | { url: string }) {
|
||||
let db: Database;
|
||||
if (config) {
|
||||
if ("database" in config && config.database) {
|
||||
if ("database" in config) {
|
||||
db = config.database;
|
||||
} else if (config.url) {
|
||||
} else {
|
||||
db = new Database(config.url);
|
||||
}
|
||||
}
|
||||
|
||||
if (!db) {
|
||||
} else {
|
||||
db = new Database(":memory:");
|
||||
}
|
||||
|
||||
return genericSqlite(
|
||||
"bun-sqlite",
|
||||
db,
|
||||
(utils) => {
|
||||
const getStmt = (sql: string) => db.prepare(sql);
|
||||
return genericSqlite("bun-sqlite", db, (utils) => {
|
||||
//const fn = cache ? "query" : "prepare";
|
||||
const getStmt = (sql: string) => db.prepare(sql);
|
||||
|
||||
return {
|
||||
db,
|
||||
query: utils.buildQueryFn({
|
||||
all: (sql, parameters) => getStmt(sql).all(...(parameters || [])),
|
||||
run: (sql, parameters) => {
|
||||
const { changes, lastInsertRowid } = getStmt(sql).run(...(parameters || []));
|
||||
return {
|
||||
insertId: utils.parseBigInt(lastInsertRowid),
|
||||
numAffectedRows: utils.parseBigInt(changes),
|
||||
};
|
||||
},
|
||||
}),
|
||||
close: () => db.close(),
|
||||
};
|
||||
},
|
||||
omitKeys(config ?? ({} as any), ["database", "url", "name", "supports"]),
|
||||
);
|
||||
return {
|
||||
db,
|
||||
query: utils.buildQueryFn({
|
||||
all: (sql, parameters) => getStmt(sql).all(...(parameters || [])),
|
||||
run: (sql, parameters) => {
|
||||
const { changes, lastInsertRowid } = getStmt(sql).run(...(parameters || []));
|
||||
return {
|
||||
insertId: utils.parseBigInt(lastInsertRowid),
|
||||
numAffectedRows: utils.parseBigInt(changes),
|
||||
};
|
||||
},
|
||||
}),
|
||||
close: () => db.close(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
describe,
|
||||
@@ -8,5 +8,4 @@ export const bunTestRunner = {
|
||||
beforeEach,
|
||||
afterEach,
|
||||
afterAll,
|
||||
beforeAll,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { inspect } from "node:util";
|
||||
|
||||
export type BindingTypeMap = {
|
||||
D1Database: D1Database;
|
||||
KVNamespace: KVNamespace;
|
||||
@@ -14,11 +12,7 @@ export function getBindings<T extends GetBindingType>(env: any, type: T): Bindin
|
||||
const bindings: BindingMap<T>[] = [];
|
||||
for (const key in env) {
|
||||
try {
|
||||
if (
|
||||
(env[key] as any).constructor.name === type ||
|
||||
String(env[key]) === `[object ${type}]` ||
|
||||
inspect(env[key]).includes(type)
|
||||
) {
|
||||
if (env[key] && (env[key] as any).constructor.name === type) {
|
||||
bindings.push({
|
||||
key,
|
||||
value: env[key] as BindingTypeMap[T],
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
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 { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import { type CloudflareBkndConfig, createApp } from "./cloudflare-workers.adapter";
|
||||
import type { CloudflareBkndConfig } from "./cloudflare-workers.adapter";
|
||||
|
||||
/* beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog); */
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("cf adapter", () => {
|
||||
const DB_URL = ":memory:";
|
||||
@@ -17,42 +18,42 @@ describe("cf adapter", () => {
|
||||
});
|
||||
|
||||
it("makes config", async () => {
|
||||
const staticConfig = await makeConfig(
|
||||
const staticConfig = makeConfig(
|
||||
{
|
||||
connection: { url: DB_URL },
|
||||
config: { data: { basepath: DB_URL } },
|
||||
initialConfig: { data: { basepath: 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();
|
||||
|
||||
const dynamicConfig = await makeConfig(
|
||||
const dynamicConfig = makeConfig(
|
||||
{
|
||||
app: (env) => ({
|
||||
config: { data: { basepath: env.DB_URL } },
|
||||
initialConfig: { data: { basepath: env.DB_URL } },
|
||||
connection: { url: env.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();
|
||||
});
|
||||
|
||||
adapterTestSuite<CloudflareBkndConfig, CloudflareContext<any>>(bunTestRunner, {
|
||||
makeApp: async (c, a) => {
|
||||
return await createApp(c, { env: a } as any);
|
||||
adapterTestSuite<CloudflareBkndConfig, CfMakeConfigArgs<any>>(bunTestRunner, {
|
||||
makeApp: async (c, a, o) => {
|
||||
return await makeApp(c, { env: a } as any, o);
|
||||
},
|
||||
makeHandler: (c, a) => {
|
||||
console.log("args", a);
|
||||
makeHandler: (c, a, o) => {
|
||||
return async (request: any) => {
|
||||
const app = await createApp(
|
||||
const app = await makeApp(
|
||||
// needs a fallback, otherwise tries to launch D1
|
||||
c ?? {
|
||||
connection: { url: DB_URL },
|
||||
},
|
||||
a as any,
|
||||
a!,
|
||||
o,
|
||||
);
|
||||
return app.fetch(request);
|
||||
};
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
import type { RuntimeBkndConfig } from "bknd/adapter";
|
||||
import { Hono } from "hono";
|
||||
import { serveStatic } from "hono/cloudflare-workers";
|
||||
import type { MaybePromise } from "bknd";
|
||||
import { $console } from "bknd/utils";
|
||||
import { createRuntimeApp } from "bknd/adapter";
|
||||
import { registerAsyncsExecutionContext, makeConfig, type CloudflareContext } from "./config";
|
||||
import { getFresh } from "./modes/fresh";
|
||||
import { getCached } from "./modes/cached";
|
||||
import { getDurable } from "./modes/durable";
|
||||
import type { App } from "bknd";
|
||||
import { $console } from "core/utils";
|
||||
import { registerMedia } from "./storage/StorageR2Adapter";
|
||||
|
||||
declare global {
|
||||
namespace Cloudflare {
|
||||
@@ -16,10 +18,12 @@ declare global {
|
||||
|
||||
export type CloudflareEnv = Cloudflare.Env;
|
||||
export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> & {
|
||||
bindings?: (args: Env) => MaybePromise<{
|
||||
mode?: "warm" | "fresh" | "cache" | "durable";
|
||||
bindings?: (args: Env) => {
|
||||
kv?: KVNamespace;
|
||||
dobj?: DurableObjectNamespace;
|
||||
db?: D1Database;
|
||||
}>;
|
||||
};
|
||||
d1?: {
|
||||
session?: boolean;
|
||||
transport?: "header" | "cookie";
|
||||
@@ -30,30 +34,14 @@ export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> &
|
||||
keepAliveSeconds?: number;
|
||||
forceHttps?: boolean;
|
||||
manifest?: string;
|
||||
registerMedia?: boolean | ((env: Env) => void);
|
||||
registerMedia?: boolean;
|
||||
};
|
||||
|
||||
export async function createApp<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
config: CloudflareBkndConfig<Env> = {},
|
||||
ctx: Partial<CloudflareContext<Env>> = {},
|
||||
) {
|
||||
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 type Context<Env = CloudflareEnv> = {
|
||||
request: Request;
|
||||
env: Env;
|
||||
ctx: ExecutionContext;
|
||||
};
|
||||
|
||||
export function serve<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
config: CloudflareBkndConfig<Env> = {},
|
||||
@@ -92,10 +80,36 @@ export function serve<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
}
|
||||
}
|
||||
|
||||
const context = { request, env, ctx } as CloudflareContext<Env>;
|
||||
const app = await createApp(config, context);
|
||||
const context = { request, env, ctx } as Context<Env>;
|
||||
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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
/// <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 { d1Sqlite } from "./connection/D1Connection";
|
||||
import { Connection } from "bknd/data";
|
||||
import type { CloudflareBkndConfig, CloudflareEnv } from ".";
|
||||
import { App } from "bknd";
|
||||
import type { Context as HonoContext, ExecutionContext } from "hono";
|
||||
import { $console } from "bknd/utils";
|
||||
import { makeConfig as makeAdapterConfig } from "bknd/adapter";
|
||||
import type { Context, ExecutionContext } from "hono";
|
||||
import { $console } from "core/utils";
|
||||
import { setCookie } from "hono/cookie";
|
||||
import { sqlite } from "bknd/adapter/sqlite";
|
||||
|
||||
export const constants = {
|
||||
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;
|
||||
ctx: ExecutionContext;
|
||||
request: Request;
|
||||
ctx?: ExecutionContext;
|
||||
request?: Request;
|
||||
};
|
||||
|
||||
function getCookieValue(cookies: string | null, name: string) {
|
||||
@@ -67,7 +66,7 @@ export function d1SessionHelper(config: CloudflareBkndConfig<any>) {
|
||||
|
||||
return undefined;
|
||||
},
|
||||
set: (c: HonoContext, d1?: D1DatabaseSession) => {
|
||||
set: (c: Context, d1?: D1DatabaseSession) => {
|
||||
if (!d1 || !config.d1?.session) return;
|
||||
|
||||
const session = d1.getBookmark();
|
||||
@@ -88,21 +87,11 @@ export function d1SessionHelper(config: CloudflareBkndConfig<any>) {
|
||||
};
|
||||
}
|
||||
|
||||
let media_registered: boolean = false;
|
||||
export async function makeConfig<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
export function makeConfig<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
config: CloudflareBkndConfig<Env>,
|
||||
args?: Partial<CloudflareContext<Env>>,
|
||||
args?: CfMakeConfigArgs<Env>,
|
||||
) {
|
||||
if (!media_registered && config.registerMedia !== false) {
|
||||
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);
|
||||
const appConfig = makeAdapterConfig(config, args?.env);
|
||||
|
||||
// if connection instance is given, don't do anything
|
||||
// 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
|
||||
} 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
|
||||
// try to make D1 from bindings
|
||||
} else if (args?.env) {
|
||||
const bindings = await config.bindings?.(args?.env);
|
||||
const bindings = config.bindings?.(args?.env);
|
||||
const sessionHelper = d1SessionHelper(config);
|
||||
const sessionId = sessionHelper.get(args.request);
|
||||
let session: D1DatabaseSession | undefined;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/// <reference types="@cloudflare/workers-types" />
|
||||
|
||||
import { genericSqlite, type GenericSqliteConnection } from "bknd";
|
||||
import { genericSqlite, type GenericSqliteConnection } from "bknd/data";
|
||||
import type { QueryResult } from "kysely";
|
||||
|
||||
export type D1SqliteConnection = GenericSqliteConnection<D1Database>;
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
/// <reference types="@cloudflare/workers-types" />
|
||||
|
||||
import { genericSqlite, type GenericSqliteConnection } from "bknd";
|
||||
import { genericSqlite, type GenericSqliteConnection } from "bknd/data";
|
||||
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 DoConnectionConfig<DB extends DurableObjecSql> =
|
||||
export type D1ConnectionConfig<DB extends DurableObjecSql> =
|
||||
| DurableObjectState
|
||||
| {
|
||||
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;
|
||||
|
||||
return genericSqlite(
|
||||
@@ -21,7 +21,7 @@ export function doSqlite<DB extends DurableObjecSql>(config: DoConnectionConfig<
|
||||
(utils) => {
|
||||
// must be async to work with the miniflare mock
|
||||
const getStmt = async (sql: string, parameters?: any[] | readonly any[]) =>
|
||||
db.exec(sql, ...(parameters || []));
|
||||
await db.exec(sql, ...(parameters || []));
|
||||
|
||||
const mapResult = (
|
||||
cursor: SqlStorageCursor<Record<string, SqlStorageValue>>,
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
/// <reference types="@cloudflare/workers-types" />
|
||||
|
||||
import { describe, beforeAll, afterAll } from "vitest";
|
||||
import { describe, test, expect } from "vitest";
|
||||
|
||||
import { viTestRunner } from "adapter/node/vitest";
|
||||
import { connectionTestSuite } from "data/connection/connection-test-suite";
|
||||
import { Miniflare } from "miniflare";
|
||||
import { doSqlite } from "./DoConnection";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
const script = `
|
||||
import { DurableObject } from "cloudflare:workers";
|
||||
@@ -41,9 +40,6 @@ export default {
|
||||
}
|
||||
`;
|
||||
|
||||
beforeAll(() => disableConsoleLog());
|
||||
afterAll(() => enableConsoleLog());
|
||||
|
||||
describe("doSqlite", async () => {
|
||||
connectionTestSuite(viTestRunner, {
|
||||
makeConnection: async () => {
|
||||
|
||||
@@ -3,10 +3,6 @@ import { cacheWorkersKV } from "./cache";
|
||||
import { viTestRunner } from "adapter/node/vitest";
|
||||
import { cacheDriverTestSuite } from "core/drivers/cache/cache-driver-test-suite";
|
||||
import { Miniflare } from "miniflare";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(() => disableConsoleLog());
|
||||
afterAll(() => enableConsoleLog());
|
||||
|
||||
describe("cacheWorkersKV", async () => {
|
||||
beforeAll(() => {
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { d1Sqlite, type D1ConnectionConfig } from "./connection/D1Connection";
|
||||
|
||||
export {
|
||||
getFresh,
|
||||
createApp,
|
||||
serve,
|
||||
type CloudflareEnv,
|
||||
type CloudflareBkndConfig,
|
||||
} from "./cloudflare-workers.adapter";
|
||||
export * from "./cloudflare-workers.adapter";
|
||||
export { makeApp, getFresh } from "./modes/fresh";
|
||||
export { getCached } from "./modes/cached";
|
||||
export { DurableBkndApp, getDurable } from "./modes/durable";
|
||||
export { d1Sqlite, type D1ConnectionConfig };
|
||||
export { doSqlite, type DoConnectionConfig } from "./connection/DoConnection";
|
||||
export {
|
||||
getBinding,
|
||||
getBindings,
|
||||
@@ -16,10 +12,8 @@ export {
|
||||
type GetBindingType,
|
||||
type BindingMap,
|
||||
} from "./bindings";
|
||||
export { constants, type CloudflareContext } from "./config";
|
||||
export { StorageR2Adapter, registerMedia } from "./storage/StorageR2Adapter";
|
||||
export { registries } from "bknd";
|
||||
export { devFsVitePlugin, devFsWrite } from "./vite";
|
||||
export { constants } from "./config";
|
||||
export { StorageR2Adapter } from "./storage/StorageR2Adapter";
|
||||
|
||||
// for compatibility with old code
|
||||
export function d1<DB extends D1Database | D1DatabaseSession = D1Database>(
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { App } from "bknd";
|
||||
import { createRuntimeApp } from "bknd/adapter";
|
||||
import type { CloudflareBkndConfig, Context, CloudflareEnv } from "../index";
|
||||
import { makeConfig, registerAsyncsExecutionContext, constants } from "../config";
|
||||
|
||||
export async function getCached<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
config: CloudflareBkndConfig<Env>,
|
||||
args: Context<Env>,
|
||||
) {
|
||||
const { env, ctx } = args;
|
||||
const { kv } = config.bindings?.(env)!;
|
||||
if (!kv) throw new Error("kv namespace is not defined in cloudflare.bindings");
|
||||
const key = config.key ?? "app";
|
||||
|
||||
const cachedConfig = await kv.get(key);
|
||||
const initialConfig = cachedConfig ? JSON.parse(cachedConfig) : undefined;
|
||||
|
||||
async function saveConfig(__config: any) {
|
||||
ctx.waitUntil(kv!.put(key, JSON.stringify(__config)));
|
||||
}
|
||||
|
||||
const app = await createRuntimeApp(
|
||||
{
|
||||
...makeConfig(config, args),
|
||||
initialConfig,
|
||||
onBuilt: async (app) => {
|
||||
registerAsyncsExecutionContext(app, ctx);
|
||||
app.module.server.client.get(constants.cache_endpoint, async (c) => {
|
||||
await kv.delete(key);
|
||||
return c.json({ message: "Cache cleared" });
|
||||
});
|
||||
await config.onBuilt?.(app);
|
||||
},
|
||||
beforeBuild: async (app) => {
|
||||
app.emgr.onEvent(
|
||||
App.Events.AppConfigUpdatedEvent,
|
||||
async ({ params: { app } }) => {
|
||||
saveConfig(app.toJSON(true));
|
||||
},
|
||||
"sync",
|
||||
);
|
||||
await config.beforeBuild?.(app);
|
||||
},
|
||||
},
|
||||
args,
|
||||
);
|
||||
|
||||
if (!cachedConfig) {
|
||||
saveConfig(app.toJSON(true));
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { DurableObject } from "cloudflare:workers";
|
||||
import type { App, CreateAppConfig } from "bknd";
|
||||
import { createRuntimeApp, makeConfig } from "bknd/adapter";
|
||||
import type { CloudflareBkndConfig, Context, CloudflareEnv } from "../index";
|
||||
import { constants, registerAsyncsExecutionContext } from "../config";
|
||||
import { $console } from "core/utils";
|
||||
|
||||
export async function getDurable<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
config: CloudflareBkndConfig<Env>,
|
||||
ctx: Context<Env>,
|
||||
) {
|
||||
const { dobj } = config.bindings?.(ctx.env)!;
|
||||
if (!dobj) throw new Error("durable object is not defined in cloudflare.bindings");
|
||||
const key = config.key ?? "app";
|
||||
|
||||
if ([config.onBuilt, config.beforeBuild].some((x) => x)) {
|
||||
$console.warn("onBuilt and beforeBuild are not supported with DurableObject mode");
|
||||
}
|
||||
|
||||
const start = performance.now();
|
||||
|
||||
const id = dobj.idFromName(key);
|
||||
const stub = dobj.get(id) as unknown as DurableBkndApp;
|
||||
|
||||
const create_config = makeConfig(config, ctx.env);
|
||||
|
||||
const res = await stub.fire(ctx.request, {
|
||||
config: create_config,
|
||||
keepAliveSeconds: config.keepAliveSeconds,
|
||||
});
|
||||
|
||||
const headers = new Headers(res.headers);
|
||||
headers.set("X-TTDO", String(performance.now() - start));
|
||||
|
||||
return new Response(res.body, {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
export class DurableBkndApp extends DurableObject {
|
||||
protected id = Math.random().toString(36).slice(2);
|
||||
protected app?: App;
|
||||
protected interval?: any;
|
||||
|
||||
async fire(
|
||||
request: Request,
|
||||
options: {
|
||||
config: CreateAppConfig;
|
||||
html?: string;
|
||||
keepAliveSeconds?: number;
|
||||
setAdminHtml?: boolean;
|
||||
},
|
||||
) {
|
||||
let buildtime = 0;
|
||||
if (!this.app) {
|
||||
const start = performance.now();
|
||||
const config = options.config;
|
||||
|
||||
// change protocol to websocket if libsql
|
||||
if (
|
||||
config?.connection &&
|
||||
"type" in config.connection &&
|
||||
config.connection.type === "libsql"
|
||||
) {
|
||||
//config.connection.config.protocol = "wss";
|
||||
}
|
||||
|
||||
this.app = await createRuntimeApp({
|
||||
...config,
|
||||
onBuilt: async (app) => {
|
||||
registerAsyncsExecutionContext(app, this.ctx);
|
||||
app.modules.server.get(constants.do_endpoint, async (c) => {
|
||||
// @ts-ignore
|
||||
const context: any = c.req.raw.cf ? c.req.raw.cf : c.env.cf;
|
||||
return c.json({
|
||||
id: this.id,
|
||||
keepAliveSeconds: options?.keepAliveSeconds ?? 0,
|
||||
colo: context.colo,
|
||||
});
|
||||
});
|
||||
|
||||
await this.onBuilt(app);
|
||||
},
|
||||
adminOptions: { html: options.html },
|
||||
beforeBuild: async (app) => {
|
||||
await this.beforeBuild(app);
|
||||
},
|
||||
});
|
||||
|
||||
buildtime = performance.now() - start;
|
||||
}
|
||||
|
||||
if (options?.keepAliveSeconds) {
|
||||
this.keepAlive(options.keepAliveSeconds);
|
||||
}
|
||||
|
||||
const res = await this.app!.fetch(request);
|
||||
const headers = new Headers(res.headers);
|
||||
headers.set("X-BuildTime", buildtime.toString());
|
||||
headers.set("X-DO-ID", this.id);
|
||||
|
||||
return new Response(res.body, {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
async onBuilt(app: App) {}
|
||||
|
||||
async beforeBuild(app: App) {}
|
||||
|
||||
protected keepAlive(seconds: number) {
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
this.interval = setInterval(() => {
|
||||
i += 1;
|
||||
if (i === seconds) {
|
||||
console.log("cleared");
|
||||
clearInterval(this.interval);
|
||||
|
||||
// ping every 30 seconds
|
||||
} else if (i % 30 === 0) {
|
||||
console.log("ping");
|
||||
this.app?.modules.ctx().connection.ping();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user