mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 08:06:00 +00:00
Compare commits
208 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2fdaf594f4 | |||
| 7d89fe4894 | |||
| 50e9be833b | |||
| 806b7427c1 | |||
| 8742938074 | |||
| 36e61cab3f | |||
| 17d4adbbfa | |||
| d052871fe0 | |||
| b0e5a49b0b | |||
| 35cbcc221a | |||
| 26d1f2b583 | |||
| 95d114ea68 | |||
| 059becbf09 | |||
| 91120091a3 | |||
| 99a812cc75 | |||
| 29a2de31c6 | |||
| 2688bf261b | |||
| 15c32c0e6d | |||
| d6f2500be2 | |||
| fa8931ad15 | |||
| 6e3060141b | |||
| 07029e3797 | |||
| 3672cc7f2c | |||
| ac32eb128e | |||
| ffdf453fea | |||
| 9dd7432e6b | |||
| 8c4a8d91a2 | |||
| 9aae6e78d6 | |||
| 317b2b50ad | |||
| a0b2dde034 | |||
| 70eaa22327 | |||
| ddfc3e599f | |||
| c11dd2bd9e | |||
| 77c85bfd5c | |||
| 7f8d8d26ac | |||
| 655bb241a8 | |||
| 1ef5bf7d15 | |||
| 9a1589da0b | |||
| f231353228 | |||
| d2de642c17 | |||
| 50c5383138 | |||
| fff88a9237 | |||
| d768a80526 | |||
| 0d79b19c66 | |||
| fea2812688 | |||
| 62368c691a | |||
| dd9617598f | |||
| a7f6d45ca9 | |||
| 92656523ff | |||
| e6ef7d9ff4 | |||
| 732cd31e1f | |||
| 1a7670f57a | |||
| af573cc79a | |||
| 1df87c8a16 | |||
| 3c5e3f9638 | |||
| 7a0a7481c0 | |||
| bdcc81b2f1 | |||
| 210b22e307 | |||
| 734235c798 | |||
| 94e168589d | |||
| c22339e4bf | |||
| d01058595f | |||
| fdec5f0693 | |||
| e8f2c70279 | |||
| 2c5371610b | |||
| 758a89b5d7 | |||
| e3888537f9 | |||
| c9773d49a6 | |||
| a559a2eabc | |||
| c9fb497c2d | |||
| ccd4eb10cb | |||
| 847f75b725 | |||
| 8b4bf0d94a | |||
| bf521e2931 | |||
| 111b2ae364 | |||
| dc08d12ce6 | |||
| ce773007f0 | |||
| e78c271c2f | |||
| 7ee07adc70 | |||
| 53e5c6fd98 | |||
| 4591631d3c | |||
| 5a27c41516 | |||
| 24eefa5357 | |||
| d898018b49 | |||
| 09fdde1c5b | |||
| fb0dbb9a51 | |||
| 81f578895e | |||
| 2de12674d9 | |||
| 5ed1cf19b6 | |||
| 5b879ac6bf | |||
| c04ad89a86 | |||
| 93019827b0 | |||
| 9436b8bac5 | |||
| 8f471a29b0 | |||
| dcf88cf587 | |||
| f9184a71db | |||
| bc1ab0f6d3 | |||
| 0b21157f92 | |||
| cacb13e464 | |||
| 8ec03afbe8 | |||
| ab8be017c1 | |||
| 5c28511fe6 | |||
| deb8aacca4 | |||
| aafd333d12 | |||
| 9e0eaf42e5 | |||
| 63254de13a | |||
| 9ac5fa03c6 | |||
| 70f0240da5 | |||
| a6ed74d904 | |||
| bd3d2ea900 | |||
| 871cec9251 | |||
| 97d6af3792 | |||
| 725a2d453a | |||
| 375d2c205f | |||
| f40ea0ec5b | |||
| 1e8c373dd4 | |||
| 2e145bbf95 | |||
| 822e6fd644 | |||
| ad04d5273e | |||
| c1c23d24fe | |||
| 06300427cb | |||
| cb873381f1 | |||
| b5b4c5eef0 | |||
| 7af8e09468 | |||
| 170ea2c45b | |||
| 42db5f55c7 | |||
| 1b02feca93 | |||
| fd966b6aef | |||
| 6bed2e4fca | |||
| dfb1457dfa | |||
| 4f945842a9 | |||
| 44fdfcc8d9 | |||
| 07810ff63c | |||
| 143d6a55ff | |||
| 8fa905a8f1 | |||
| ad0d2e6ff8 | |||
| aa0e6f90d9 | |||
| 3e2938f77d | |||
| 5e5f0ef70f | |||
| 104885ea5c | |||
| ffbb61d58a | |||
| bd48bb7a18 | |||
| a298b65abf | |||
| daaaae82b6 | |||
| 79ace4f1e5 | |||
| f19977ac28 | |||
| a3782728f9 | |||
| 8b832f02ac | |||
| cec32ae881 | |||
| 0fffa26d55 | |||
| 2b6f520f46 | |||
| 7379bf2b1b | |||
| db3c2cea10 | |||
| 2b32a836cd | |||
| 957355c862 | |||
| fc282b795e | |||
| bc65f5f311 | |||
| 5355f2593e | |||
| e939debba5 | |||
| b35ee36fb1 | |||
| 109c72e84f | |||
| 7e0b02a125 | |||
| d4076803c6 | |||
| a5959acb34 | |||
| a4ddf780ce | |||
| 2f684765de | |||
| 22b54862e8 | |||
| 45138c25f0 | |||
| 80034b9b0a | |||
| e6ee75c712 | |||
| 56287eb05e | |||
| 144f35ec66 | |||
| d1378c6c51 | |||
| 4e10b36d0d | |||
| 70737c04cd | |||
| d41fd5541f | |||
| e76e3531d7 | |||
| a9f3a582eb | |||
| 57ae2f333c | |||
| b2086c4da7 | |||
| 344d729320 | |||
| c86f4c12b7 | |||
| aaa97ed113 | |||
| 69c8aec6fb | |||
| f5ceffd80e | |||
| a9f367aa2c | |||
| b87696a0db | |||
| 3338804c34 | |||
| 6b3ac9e6e2 | |||
| 8e1ecfcfe3 | |||
| 6e2596fb77 | |||
| 2ada4e9f20 | |||
| cc038a0a9a | |||
| af6d1960b9 | |||
| 96ae8f9d63 | |||
| c09d1d53b3 | |||
| 2239333f08 | |||
| 0710b5bf1f | |||
| 03c4c41d2d | |||
| 85d8542a2e | |||
| f8d2a9090e | |||
| bbb7bfb7a1 | |||
| 28e277afe1 | |||
| 8517c9b90b | |||
| fe5ccd4206 | |||
| 8b4b63b3cd | |||
| 046c1d21b1 | |||
| 6c2e579596 |
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"bknd": {
|
||||
"url": "http://localhost:28623/api/system/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,15 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22.x"
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: "1.2.14"
|
||||
bun-version: "1.2.19"
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./app
|
||||
|
||||
+3
-1
@@ -31,4 +31,6 @@ packages/media/.env
|
||||
.git_old
|
||||
docker/tmp
|
||||
.debug
|
||||
.history
|
||||
.history
|
||||
.aider*
|
||||
.vercel
|
||||
|
||||
@@ -1,26 +1,34 @@
|
||||
[](https://npmjs.org/package/bknd)
|
||||
|
||||

|
||||

|
||||
|
||||
<p align="center" width="100%">
|
||||
<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">
|
||||
<a href="https://stackblitz.com/github/bknd-io/bknd-demo?hideExplorer=1&embed=1&view=preview&initialPath=%2Fdata%2Fschema" target="_blank">
|
||||
<strong>⭐ Live Demo</strong>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
bknd simplifies app development by providing a fully functional backend for database management, authentication, media and workflows. Being lightweight and built on Web Standards, it can be deployed nearly anywhere, including running inside your framework of choice. No more deploying multiple separate services!
|
||||
* **Runtimes**: Node.js 22+, Bun 1.0+, Deno, Browser, Cloudflare Workers/Pages, Vercel, Netlify, AWS Lambda, etc.
|
||||
* **Databases**:
|
||||
* SQLite: LibSQL, Node SQLite, Bun SQLite, Cloudflare D1, Cloudflare Durable Objects SQLite, SQLocal
|
||||
* Postgres: Vanilla Postgres, Supabase, Neon, Xata
|
||||
* **Frameworks**: React, Next.js, React Router, Astro, Vite, Waku
|
||||
* **Storage**: AWS S3, S3-compatible (Tigris, R2, Minio, etc.), Cloudflare R2 (binding), Cloudinary, Filesystem
|
||||
|
||||
**For documentation and examples, please visit https://docs.bknd.io.**
|
||||
|
||||
> [!WARNING]
|
||||
> This project requires Node.js 22.13 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.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -44,6 +52,7 @@ 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:
|
||||
|
||||
+134
-13
@@ -1,27 +1,148 @@
|
||||
import { afterAll, afterEach, describe, expect, test } from "bun:test";
|
||||
import { App } from "../src";
|
||||
import { afterEach, describe, test, expect, beforeAll, afterAll } 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(afterAllCleanup);
|
||||
afterEach(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||
|
||||
describe("App tests", async () => {
|
||||
test("boots and pongs", async () => {
|
||||
const app = new App(dummyConnection);
|
||||
await app.build();
|
||||
|
||||
//expect(await app.data?.em.ping()).toBeTrue();
|
||||
expect(await app.em.ping()).toBeTrue();
|
||||
});
|
||||
|
||||
/*test.only("what", async () => {
|
||||
const app = new App(dummyConnection, {
|
||||
auth: {
|
||||
enabled: true,
|
||||
test("plugins", async () => {
|
||||
const called: string[] = [];
|
||||
const app = createApp({
|
||||
config: {
|
||||
auth: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
options: {
|
||||
plugins: [
|
||||
(app) => {
|
||||
expect(app).toBeDefined();
|
||||
expect(app).toBeInstanceOf(App);
|
||||
return {
|
||||
name: "test",
|
||||
schema: () => {
|
||||
called.push("schema");
|
||||
return proto.em(
|
||||
{
|
||||
posts: proto.entity("posts", {
|
||||
title: proto.text(),
|
||||
}),
|
||||
comments: proto.entity("comments", {
|
||||
content: proto.text(),
|
||||
}),
|
||||
users: proto.entity("users", {
|
||||
email_verified: proto.boolean(),
|
||||
}),
|
||||
},
|
||||
(fn, s) => {
|
||||
fn.relation(s.comments).manyToOne(s.posts);
|
||||
fn.index(s.posts).on(["title"]);
|
||||
},
|
||||
);
|
||||
},
|
||||
onBoot: async () => {
|
||||
called.push("onBoot");
|
||||
},
|
||||
beforeBuild: async () => {
|
||||
called.push("beforeBuild");
|
||||
},
|
||||
onBuilt: async () => {
|
||||
called.push("onBuilt");
|
||||
},
|
||||
onServerInit: async (server) => {
|
||||
called.push("onServerInit");
|
||||
expect(server).toBeDefined();
|
||||
expect(server).toBeInstanceOf(Hono);
|
||||
},
|
||||
onFirstBoot: async () => {
|
||||
called.push("onFirstBoot");
|
||||
},
|
||||
};
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await app.module.auth.build();
|
||||
await app.module.data.build();
|
||||
console.log(app.em.entities.map((e) => e.name));
|
||||
console.log(await app.em.schema().getDiff());
|
||||
});*/
|
||||
|
||||
await app.build();
|
||||
|
||||
expect(app.em.entities.map((e) => e.name)).toEqual(["users", "posts", "comments"]);
|
||||
expect(app.em.indices.map((i) => i.name)).toEqual([
|
||||
"idx_unique_users_email",
|
||||
"idx_users_strategy",
|
||||
"idx_users_strategy_value",
|
||||
"idx_posts_title",
|
||||
]);
|
||||
expect(
|
||||
app.em.relations.all.map((r) => pick(r.toJSON(), ["type", "source", "target"])),
|
||||
).toEqual([
|
||||
{
|
||||
type: "n:1",
|
||||
source: "comments",
|
||||
target: "posts",
|
||||
},
|
||||
]);
|
||||
expect(called).toEqual([
|
||||
"onBoot",
|
||||
"onServerInit",
|
||||
"beforeBuild",
|
||||
"onServerInit",
|
||||
"schema",
|
||||
"onFirstBoot",
|
||||
"onBuilt",
|
||||
]);
|
||||
expect(app.plugins.size).toBe(1);
|
||||
expect(Array.from(app.plugins.keys())).toEqual(["test"]);
|
||||
});
|
||||
|
||||
test.only("drivers", async () => {
|
||||
const called: string[] = [];
|
||||
const app = new App(dummyConnection, undefined, {
|
||||
drivers: {
|
||||
email: {
|
||||
send: async (to, subject, body) => {
|
||||
called.push("email.send");
|
||||
return {
|
||||
id: "",
|
||||
};
|
||||
},
|
||||
},
|
||||
cache: {
|
||||
get: async (key) => {
|
||||
called.push("cache.get");
|
||||
return "";
|
||||
},
|
||||
set: async (key, value, ttl) => {
|
||||
called.push("cache.set");
|
||||
},
|
||||
del: async (key) => {
|
||||
called.push("cache.del");
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await app.build();
|
||||
|
||||
expect(app.drivers.cache).toBeDefined();
|
||||
expect(app.drivers.email).toBeDefined();
|
||||
await app.drivers.email.send("", "", "");
|
||||
await app.drivers.cache.get("");
|
||||
await app.drivers.cache.set("", "", 0);
|
||||
await app.drivers.cache.del("");
|
||||
|
||||
expect(called).toEqual(["email.send", "cache.get", "cache.set", "cache.del"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
tmp/*
|
||||
tmp/*
|
||||
!tmp/.gitkeep
|
||||
@@ -1,53 +1,57 @@
|
||||
import { expect, describe, it, beforeAll, afterAll } from "bun:test";
|
||||
import { expect, describe, it, beforeAll, afterAll, mock } from "bun:test";
|
||||
import * as adapter from "adapter";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import { omitKeys } from "core/utils";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("adapter", () => {
|
||||
it("makes config", () => {
|
||||
expect(adapter.makeConfig({})).toEqual({});
|
||||
expect(adapter.makeConfig({}, { env: { TEST: "test" } })).toEqual({});
|
||||
it("makes config", async () => {
|
||||
expect(omitKeys(await adapter.makeConfig({}), ["connection"])).toEqual({});
|
||||
expect(
|
||||
omitKeys(await adapter.makeConfig({}, { env: { TEST: "test" } }), ["connection"]),
|
||||
).toEqual({});
|
||||
|
||||
// merges everything returned from `app` with the config
|
||||
expect(adapter.makeConfig({ app: (a) => a as any }, { env: { TEST: "test" } })).toEqual({
|
||||
env: { TEST: "test" },
|
||||
} as any);
|
||||
expect(
|
||||
omitKeys(
|
||||
await adapter.makeConfig(
|
||||
{ app: (a) => ({ config: { server: { cors: { origin: a.env.TEST } } } }) },
|
||||
{ env: { TEST: "test" } },
|
||||
),
|
||||
["connection"],
|
||||
),
|
||||
).toEqual({
|
||||
config: { server: { cors: { origin: "test" } } },
|
||||
});
|
||||
});
|
||||
|
||||
it("reuses apps correctly", async () => {
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
const first = await adapter.createAdapterApp(
|
||||
it("allows all properties in app function", async () => {
|
||||
const called = mock(() => null);
|
||||
const config = await adapter.makeConfig(
|
||||
{
|
||||
initialConfig: { server: { cors: { origin: "random" } } },
|
||||
app: (env) => ({
|
||||
connection: { url: "test" },
|
||||
config: { server: { cors: { origin: "test" } } },
|
||||
options: {
|
||||
mode: "db",
|
||||
},
|
||||
onBuilt: () => {
|
||||
called();
|
||||
expect(env).toEqual({ foo: "bar" });
|
||||
},
|
||||
}),
|
||||
},
|
||||
undefined,
|
||||
{ id },
|
||||
{ foo: "bar" },
|
||||
);
|
||||
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("*");
|
||||
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();
|
||||
});
|
||||
|
||||
adapterTestSuite(bunTestRunner, {
|
||||
|
||||
@@ -42,7 +42,6 @@ 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";
|
||||
import { parse } from "../../src/core/utils";
|
||||
import { Guard } from "../../src/auth/authorize/Guard";
|
||||
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";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
@@ -153,7 +153,7 @@ describe("DataApi", () => {
|
||||
const oneBy = api.readOneBy("posts", { where: { title: "baz" }, select: ["title"] });
|
||||
const oneByRes = await oneBy;
|
||||
expect(oneByRes.data).toEqual({ title: "baz" } as any);
|
||||
expect(oneByRes.body.meta.count).toEqual(1);
|
||||
expect(oneByRes.body.meta.items).toEqual(1);
|
||||
});
|
||||
|
||||
it("exists/count", async () => {
|
||||
@@ -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,9 +1,23 @@
|
||||
import { describe, expect, mock, test } from "bun:test";
|
||||
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
|
||||
import type { ModuleBuildContext } from "../../src";
|
||||
import { App, createApp } from "../../src/App";
|
||||
import * as proto from "../../src/data/prototype";
|
||||
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);
|
||||
|
||||
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({
|
||||
@@ -20,6 +34,8 @@ describe("App", () => {
|
||||
"guard",
|
||||
"flags",
|
||||
"logger",
|
||||
"mcp",
|
||||
"helper",
|
||||
]);
|
||||
},
|
||||
},
|
||||
@@ -27,7 +43,7 @@ describe("App", () => {
|
||||
expect(called).toHaveBeenCalled();
|
||||
|
||||
const app = createApp({
|
||||
initialConfig: {
|
||||
config: {
|
||||
data: proto
|
||||
.em({
|
||||
todos: proto.entity("todos", {
|
||||
@@ -134,4 +150,21 @@ 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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { AppServer, serverConfigSchema } from "modules/server/AppServer";
|
||||
import { describe, test, expect } from "bun:test";
|
||||
|
||||
describe("AppServer", () => {
|
||||
test("config", () => {
|
||||
{
|
||||
const server = new 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",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const server = new AppServer({
|
||||
cors: {
|
||||
origin: "https",
|
||||
allow_methods: ["GET", "POST"],
|
||||
},
|
||||
});
|
||||
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",
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,226 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,346 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
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");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
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());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
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,7 +1,12 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { createApp, registries } from "../../src";
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { registries } from "../../src";
|
||||
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 () => {
|
||||
/**
|
||||
@@ -87,7 +92,7 @@ describe("repros", async () => {
|
||||
fns.relation(schema.product_likes).manyToOne(schema.users);
|
||||
},
|
||||
);
|
||||
const app = createApp({ initialConfig: { data: schema.toJSON() } });
|
||||
const app = createApp({ config: { data: schema.toJSON() } });
|
||||
await app.build();
|
||||
|
||||
const info = (await (await app.server.request("/api/data/info/products")).json()) as any;
|
||||
|
||||
@@ -1,46 +1,3 @@
|
||||
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 * as hash from "../../src/auth/utils/hash";
|
||||
import { Default, parse } from "../../src/core/utils";
|
||||
|
||||
/*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(Default(cookieConfig, {}));
|
||||
});
|
||||
/*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);
|
||||
});*/
|
||||
});
|
||||
describe("Authenticator", async () => {});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Guard } from "../../../src/auth";
|
||||
import { Guard } from "../../../src/auth/authorize/Guard";
|
||||
|
||||
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 "../helper";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { type TObject, type TString, Type } from "@sinclair/typebox";
|
||||
import { Registry } from "core";
|
||||
import { Registry } from "core/registry/Registry";
|
||||
import { s } from "core/utils/schema";
|
||||
|
||||
type Constructor<T> = new (...args: any[]) => T;
|
||||
|
||||
@@ -11,7 +11,7 @@ class What {
|
||||
return null;
|
||||
}
|
||||
getType() {
|
||||
return Type.Object({ type: Type.String() });
|
||||
return s.object({ type: s.string() });
|
||||
}
|
||||
}
|
||||
class What2 extends What {}
|
||||
@@ -19,7 +19,7 @@ class NotAllowed {}
|
||||
|
||||
type Test1 = {
|
||||
cls: new (...args: any[]) => What;
|
||||
schema: TObject<{ type: TString }>;
|
||||
schema: s.ObjectSchema<{ type: s.StringSchema }>;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@ describe("Registry", () => {
|
||||
const registry = new Registry<Test1>().set({
|
||||
first: {
|
||||
cls: What,
|
||||
schema: Type.Object({ type: Type.String(), what: Type.String() }),
|
||||
schema: s.object({ type: s.string(), what: s.string() }),
|
||||
enabled: true,
|
||||
},
|
||||
} satisfies Record<string, Test1>);
|
||||
@@ -37,7 +37,7 @@ describe("Registry", () => {
|
||||
expect(item).toBeDefined();
|
||||
expect(item?.cls).toBe(What);
|
||||
|
||||
const second = Type.Object({ type: Type.String(), what: Type.String() });
|
||||
const second = s.object({ type: s.string(), what: s.string() });
|
||||
registry.add("second", {
|
||||
cls: What2,
|
||||
schema: second,
|
||||
@@ -46,7 +46,7 @@ describe("Registry", () => {
|
||||
// @ts-ignore
|
||||
expect(registry.get("second").schema).toEqual(second);
|
||||
|
||||
const third = Type.Object({ type: Type.String({ default: "1" }), what22: Type.String() });
|
||||
const third = s.object({ type: s.string({ default: "1" }), what22: s.string() });
|
||||
registry.add("third", {
|
||||
// @ts-expect-error
|
||||
cls: NotAllowed,
|
||||
@@ -56,7 +56,7 @@ describe("Registry", () => {
|
||||
// @ts-ignore
|
||||
expect(registry.get("third").schema).toEqual(third);
|
||||
|
||||
const fourth = Type.Object({ type: Type.Number(), what22: Type.String() });
|
||||
const fourth = s.object({ type: s.number(), what22: s.string() });
|
||||
registry.add("fourth", {
|
||||
cls: What,
|
||||
// @ts-expect-error
|
||||
@@ -81,6 +81,8 @@ describe("Registry", () => {
|
||||
registry.register("what2", What2);
|
||||
expect(registry.get("what2")).toBeDefined();
|
||||
expect(registry.get("what2").cls).toBe(What2);
|
||||
expect(registry.get("what2").schema).toEqual(What2.prototype.getType());
|
||||
expect(JSON.stringify(registry.get("what2").schema)).toEqual(
|
||||
JSON.stringify(What2.prototype.getType()),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { SchemaObject } from "../../../src/core";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { s } from "core/utils/schema";
|
||||
import { SchemaObject } from "core/object/SchemaObject";
|
||||
|
||||
describe("SchemaObject", async () => {
|
||||
test("basic", async () => {
|
||||
const m = new SchemaObject(
|
||||
Type.Object({ a: Type.String({ default: "b" }) }),
|
||||
s.strictObject({ a: s.string({ default: "b" }) }),
|
||||
{ a: "test" },
|
||||
{
|
||||
forceParse: true,
|
||||
@@ -23,19 +23,19 @@ describe("SchemaObject", async () => {
|
||||
|
||||
test("patch", async () => {
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
s: Type.Object(
|
||||
s.strictObject({
|
||||
s: s.strictObject(
|
||||
{
|
||||
a: Type.String({ default: "b" }),
|
||||
b: Type.Object(
|
||||
a: s.string({ default: "b" }),
|
||||
b: s.strictObject(
|
||||
{
|
||||
c: Type.String({ default: "d" }),
|
||||
e: Type.String({ default: "f" }),
|
||||
c: s.string({ default: "d" }),
|
||||
e: s.string({ default: "f" }),
|
||||
},
|
||||
{ default: {} },
|
||||
),
|
||||
},
|
||||
{ default: {}, additionalProperties: false },
|
||||
{ default: {} },
|
||||
),
|
||||
}),
|
||||
);
|
||||
@@ -44,7 +44,7 @@ describe("SchemaObject", async () => {
|
||||
await m.patch("s.a", "c");
|
||||
|
||||
// non-existing path on no additional properties
|
||||
expect(() => m.patch("s.s.s", "c")).toThrow();
|
||||
expect(m.patch("s.s.s", "c")).rejects.toThrow();
|
||||
// wrong type
|
||||
expect(() => m.patch("s.a", 1)).toThrow();
|
||||
|
||||
@@ -58,8 +58,8 @@ describe("SchemaObject", async () => {
|
||||
|
||||
test("patch array", async () => {
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
|
||||
s.strictObject({
|
||||
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
||||
}),
|
||||
);
|
||||
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
|
||||
@@ -75,13 +75,13 @@ describe("SchemaObject", async () => {
|
||||
|
||||
test("remove", async () => {
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
s: Type.Object(
|
||||
s.object({
|
||||
s: s.object(
|
||||
{
|
||||
a: Type.String({ default: "b" }),
|
||||
b: Type.Object(
|
||||
a: s.string({ default: "b" }),
|
||||
b: s.object(
|
||||
{
|
||||
c: Type.String({ default: "d" }),
|
||||
c: s.string({ default: "d" }),
|
||||
},
|
||||
{ default: {} },
|
||||
),
|
||||
@@ -107,8 +107,8 @@ describe("SchemaObject", async () => {
|
||||
|
||||
test("set", async () => {
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
|
||||
s.strictObject({
|
||||
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
||||
}),
|
||||
);
|
||||
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
|
||||
@@ -124,8 +124,8 @@ describe("SchemaObject", async () => {
|
||||
let called = false;
|
||||
let result: any;
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
|
||||
s.strictObject({
|
||||
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
||||
}),
|
||||
undefined,
|
||||
{
|
||||
@@ -145,8 +145,8 @@ describe("SchemaObject", async () => {
|
||||
test("listener: onBeforeUpdate", async () => {
|
||||
let called = false;
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
|
||||
s.strictObject({
|
||||
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
||||
}),
|
||||
undefined,
|
||||
{
|
||||
@@ -167,7 +167,7 @@ describe("SchemaObject", async () => {
|
||||
});
|
||||
|
||||
test("throwIfRestricted", async () => {
|
||||
const m = new SchemaObject(Type.Object({}), undefined, {
|
||||
const m = new SchemaObject(s.strictObject({}), undefined, {
|
||||
restrictPaths: ["a.b"],
|
||||
});
|
||||
|
||||
@@ -179,13 +179,13 @@ describe("SchemaObject", async () => {
|
||||
|
||||
test("restriction bypass", async () => {
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
s: Type.Object(
|
||||
s.strictObject({
|
||||
s: s.strictObject(
|
||||
{
|
||||
a: Type.String({ default: "b" }),
|
||||
b: Type.Object(
|
||||
a: s.string({ default: "b" }),
|
||||
b: s.strictObject(
|
||||
{
|
||||
c: Type.String({ default: "d" }),
|
||||
c: s.string({ default: "d" }),
|
||||
},
|
||||
{ default: {} },
|
||||
),
|
||||
@@ -205,7 +205,21 @@ describe("SchemaObject", async () => {
|
||||
expect(m.get()).toEqual({ s: { a: "b", b: { c: "e" } } });
|
||||
});
|
||||
|
||||
const dataEntitiesSchema = Type.Object(
|
||||
const dataEntitiesSchema = s.strictObject({
|
||||
entities: s.record(
|
||||
s.object({
|
||||
fields: s.record(
|
||||
s.object({
|
||||
type: s.string(),
|
||||
config: s.object({}).optional(),
|
||||
}),
|
||||
),
|
||||
config: s.record(s.string()).optional(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
/* const dataEntitiesSchema = Type.Object(
|
||||
{
|
||||
entities: Type.Object(
|
||||
{},
|
||||
@@ -230,7 +244,7 @@ describe("SchemaObject", async () => {
|
||||
{
|
||||
additionalProperties: false,
|
||||
},
|
||||
);
|
||||
); */
|
||||
test("patch safe object, overwrite", async () => {
|
||||
const data = {
|
||||
entities: {
|
||||
|
||||
@@ -248,7 +248,7 @@ describe("Core Utils", async () => {
|
||||
expect(utils.getContentName(request)).toBe(name);
|
||||
});
|
||||
|
||||
test.only("detectImageDimensions", async () => {
|
||||
test("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.only("formats local time", () => {
|
||||
test("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,72 +1,24 @@
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
|
||||
import { Guard } from "../../src/auth";
|
||||
import { parse } from "../../src/core/utils";
|
||||
import {
|
||||
Entity,
|
||||
type EntityData,
|
||||
EntityManager,
|
||||
ManyToOneRelation,
|
||||
type MutatorResponse,
|
||||
type RepositoryResponse,
|
||||
TextField,
|
||||
} from "../../src/data";
|
||||
import { Guard } from "../../src/auth/authorize/Guard";
|
||||
import { parse } from "core/utils/schema";
|
||||
|
||||
import { DataController } from "../../src/data/api/DataController";
|
||||
import { dataConfigSchema } from "../../src/data/data-schema";
|
||||
import { disableConsoleLog, enableConsoleLog, getDummyConnection } from "../helper";
|
||||
import { getDummyConnection } from "../helper";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
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(["log", "warn"]));
|
||||
beforeAll(() => disableConsoleLog());
|
||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||
|
||||
const dataConfig = parse(dataConfigSchema, {});
|
||||
describe("[data] DataController", async () => {
|
||||
test("repoResult", async () => {
|
||||
const em = new EntityManager<any>([], dummyConnection);
|
||||
const ctx: any = { em, guard: new Guard() };
|
||||
const controller = new DataController(ctx, dataConfig);
|
||||
|
||||
const res = controller.repoResult({
|
||||
entity: null as any,
|
||||
data: [] as any,
|
||||
sql: "",
|
||||
parameters: [] as any,
|
||||
result: [] as any,
|
||||
meta: {
|
||||
total: 0,
|
||||
count: 0,
|
||||
items: 0,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
meta: {
|
||||
total: 0,
|
||||
count: 0,
|
||||
items: 0,
|
||||
},
|
||||
data: [],
|
||||
});
|
||||
});
|
||||
|
||||
test("mutatorResult", async () => {
|
||||
const em = new EntityManager([], dummyConnection);
|
||||
const ctx: any = { em, guard: new Guard() };
|
||||
const controller = new DataController(ctx, dataConfig);
|
||||
|
||||
const res = controller.mutatorResult({
|
||||
entity: null as any,
|
||||
data: [] as any,
|
||||
sql: "",
|
||||
parameters: [] as any,
|
||||
result: [] as any,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
data: [],
|
||||
});
|
||||
});
|
||||
|
||||
describe("getController", async () => {
|
||||
const users = new Entity("users", [
|
||||
new TextField("name", { required: true }),
|
||||
@@ -120,8 +72,7 @@ describe("[data] DataController", async () => {
|
||||
method: "POST",
|
||||
body: JSON.stringify(_user),
|
||||
});
|
||||
//console.log("res", { _user }, res);
|
||||
const result = (await res.json()) as MutatorResponse;
|
||||
const result = (await res.json()) as MutatorResultJSON;
|
||||
const { id, ...data } = result.data as any;
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
@@ -135,7 +86,7 @@ describe("[data] DataController", async () => {
|
||||
method: "POST",
|
||||
body: JSON.stringify(_post),
|
||||
});
|
||||
const result = (await res.json()) as MutatorResponse;
|
||||
const result = (await res.json()) as MutatorResultJSON;
|
||||
const { id, ...data } = result.data as any;
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
@@ -146,13 +97,13 @@ describe("[data] DataController", async () => {
|
||||
|
||||
test("/:entity (read many)", async () => {
|
||||
const res = await app.request("/entity/users");
|
||||
const data = (await res.json()) as RepositoryResponse;
|
||||
const data = (await res.json()) as RepositoryResultJSON;
|
||||
|
||||
expect(data.meta.total).toBe(3);
|
||||
expect(data.meta.count).toBe(3);
|
||||
//expect(data.meta.total).toBe(3);
|
||||
//expect(data.meta.count).toBe(3);
|
||||
expect(data.meta.items).toBe(3);
|
||||
expect(data.data.length).toBe(3);
|
||||
expect(data.data[0].name).toBe("foo");
|
||||
expect(data.data[0]?.name).toBe("foo");
|
||||
});
|
||||
|
||||
test("/:entity/query (func query)", async () => {
|
||||
@@ -165,33 +116,32 @@ describe("[data] DataController", async () => {
|
||||
where: { bio: { $isnull: 1 } },
|
||||
}),
|
||||
});
|
||||
const data = (await res.json()) as RepositoryResponse;
|
||||
const data = (await res.json()) as RepositoryResultJSON;
|
||||
|
||||
expect(data.meta.total).toBe(3);
|
||||
expect(data.meta.count).toBe(1);
|
||||
//expect(data.meta.total).toBe(3);
|
||||
//expect(data.meta.count).toBe(1);
|
||||
expect(data.meta.items).toBe(1);
|
||||
expect(data.data.length).toBe(1);
|
||||
expect(data.data[0].name).toBe("bar");
|
||||
expect(data.data[0]?.name).toBe("bar");
|
||||
});
|
||||
|
||||
test("/:entity (read many, paginated)", async () => {
|
||||
const res = await app.request("/entity/users?limit=1&offset=2");
|
||||
const data = (await res.json()) as RepositoryResponse;
|
||||
const data = (await res.json()) as RepositoryResultJSON;
|
||||
|
||||
expect(data.meta.total).toBe(3);
|
||||
expect(data.meta.count).toBe(3);
|
||||
//expect(data.meta.total).toBe(3);
|
||||
//expect(data.meta.count).toBe(3);
|
||||
expect(data.meta.items).toBe(1);
|
||||
expect(data.data.length).toBe(1);
|
||||
expect(data.data[0].name).toBe("baz");
|
||||
expect(data.data[0]?.name).toBe("baz");
|
||||
});
|
||||
|
||||
test("/:entity/:id (read one)", async () => {
|
||||
const res = await app.request("/entity/users/3");
|
||||
const data = (await res.json()) as RepositoryResponse<EntityData>;
|
||||
console.log("data", data);
|
||||
const data = (await res.json()) as RepositoryResultJSON<EntityData>;
|
||||
|
||||
expect(data.meta.total).toBe(3);
|
||||
expect(data.meta.count).toBe(1);
|
||||
//expect(data.meta.total).toBe(3);
|
||||
//expect(data.meta.count).toBe(1);
|
||||
expect(data.meta.items).toBe(1);
|
||||
expect(data.data).toEqual({ id: 3, ...fixtures.users[2] });
|
||||
});
|
||||
@@ -201,7 +151,7 @@ describe("[data] DataController", async () => {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ name: "new name" }),
|
||||
});
|
||||
const { data } = (await res.json()) as MutatorResponse;
|
||||
const { data } = (await res.json()) as MutatorResultJSON;
|
||||
|
||||
expect(res.ok).toBe(true);
|
||||
expect(data as any).toEqual({ id: 3, ...fixtures.users[2], name: "new name" });
|
||||
@@ -209,27 +159,26 @@ describe("[data] DataController", async () => {
|
||||
|
||||
test("/:entity/:id/:reference (read references)", async () => {
|
||||
const res = await app.request("/entity/users/1/posts");
|
||||
const data = (await res.json()) as RepositoryResponse;
|
||||
console.log("data", data);
|
||||
const data = (await res.json()) as RepositoryResultJSON;
|
||||
|
||||
expect(data.meta.total).toBe(2);
|
||||
expect(data.meta.count).toBe(1);
|
||||
//expect(data.meta.total).toBe(2);
|
||||
//expect(data.meta.count).toBe(1);
|
||||
expect(data.meta.items).toBe(1);
|
||||
expect(data.data.length).toBe(1);
|
||||
expect(data.data[0].content).toBe("post 1");
|
||||
expect(data.data[0]?.content).toBe("post 1");
|
||||
});
|
||||
|
||||
test("/:entity/:id (delete one)", async () => {
|
||||
const res = await app.request("/entity/posts/2", {
|
||||
method: "DELETE",
|
||||
});
|
||||
const { data } = (await res.json()) as RepositoryResponse<EntityData>;
|
||||
const { data } = (await res.json()) as RepositoryResultJSON<EntityData>;
|
||||
expect(data).toEqual({ id: 2, ...fixtures.posts[1] });
|
||||
|
||||
// verify
|
||||
const res2 = await app.request("/entity/posts");
|
||||
const data2 = (await res2.json()) as RepositoryResponse;
|
||||
expect(data2.meta.total).toBe(1);
|
||||
const data2 = (await res2.json()) as RepositoryResultJSON;
|
||||
//expect(data2.meta.total).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
NumberField,
|
||||
PrimaryField,
|
||||
Repository,
|
||||
TextField,
|
||||
} from "../../src/data";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { TextField, PrimaryField, NumberField } from "data/fields";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
@@ -34,19 +28,12 @@ describe("some tests", async () => {
|
||||
|
||||
test("findId", async () => {
|
||||
const query = await em.repository(users).findId(1);
|
||||
/*const { result, total, count, time } = query;
|
||||
console.log("query", query.result, {
|
||||
result,
|
||||
total,
|
||||
count,
|
||||
time,
|
||||
});*/
|
||||
|
||||
expect(query.sql).toBe(
|
||||
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" where "id" = ? limit ?',
|
||||
);
|
||||
expect(query.parameters).toEqual([1, 1]);
|
||||
expect(query.result).toEqual([]);
|
||||
expect(query.data).toBeUndefined();
|
||||
});
|
||||
|
||||
test("findMany", async () => {
|
||||
@@ -56,7 +43,7 @@ describe("some tests", async () => {
|
||||
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" order by "users"."id" asc limit ? offset ?',
|
||||
);
|
||||
expect(query.parameters).toEqual([10, 0]);
|
||||
expect(query.result).toEqual([]);
|
||||
expect(query.data).toEqual([]);
|
||||
});
|
||||
|
||||
test("findMany with number", async () => {
|
||||
@@ -66,7 +53,7 @@ describe("some tests", async () => {
|
||||
'select "posts"."id" as "id", "posts"."title" as "title", "posts"."content" as "content", "posts"."created_at" as "created_at", "posts"."likes" as "likes" from "posts" order by "posts"."id" asc limit ? offset ?',
|
||||
);
|
||||
expect(query.parameters).toEqual([10, 0]);
|
||||
expect(query.result).toEqual([]);
|
||||
expect(query.data).toEqual([]);
|
||||
});
|
||||
|
||||
test("try adding an existing field name", async () => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { unlink } from "node:fs/promises";
|
||||
import type { SqliteDatabase } from "kysely";
|
||||
// @ts-ignore
|
||||
import Database from "libsql";
|
||||
import { SqliteLocalConnection } from "../../src/data";
|
||||
import { SqliteLocalConnection } from "data/connection/sqlite/SqliteLocalConnection";
|
||||
|
||||
export function getDummyDatabase(memory: boolean = true): {
|
||||
dummyDb: SqliteDatabase;
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
ManyToOneRelation,
|
||||
NumberField,
|
||||
SchemaManager,
|
||||
TextField,
|
||||
} from "../../src/data";
|
||||
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 { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { Entity, EntityManager, Mutator, NumberField, TextField } from "../../src/data";
|
||||
import { TransformPersistFailedException } from "../../src/data/errors";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { NumberField, TextField } from "data/fields";
|
||||
import { TransformPersistFailedException } from "data/errors";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
@@ -45,7 +46,7 @@ describe("Mutator simple", async () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(query.result).toEqual([{ id: 1, label: "test", count: 1 }]);
|
||||
expect(query.data).toEqual([{ id: 1, label: "test", count: 1 }]);
|
||||
});
|
||||
|
||||
test("update inserted row", async () => {
|
||||
@@ -87,7 +88,7 @@ describe("Mutator simple", async () => {
|
||||
expect(mutation.data).toEqual({ id, label: "new label", count: 100 });
|
||||
|
||||
const query2 = await em.repository(items).findId(id);
|
||||
expect(query2.result.length).toBe(0);
|
||||
expect(query2.data).toBeUndefined();
|
||||
});
|
||||
|
||||
test("validation: insert incomplete row", async () => {
|
||||
@@ -177,13 +178,13 @@ describe("Mutator simple", async () => {
|
||||
});
|
||||
|
||||
test("insertMany", async () => {
|
||||
const oldCount = (await em.repo(items).count()).count;
|
||||
const oldCount = (await em.repo(items).count()).data.count;
|
||||
const inserts = [{ label: "insert 1" }, { label: "insert 2" }];
|
||||
const { data } = await em.mutator(items).insertMany(inserts);
|
||||
|
||||
expect(data.length).toBe(2);
|
||||
expect(data.map((d) => ({ label: d.label }))).toEqual(inserts);
|
||||
const newCount = (await em.repo(items).count()).count;
|
||||
const newCount = (await em.repo(items).count()).data.count;
|
||||
expect(newCount).toBe(oldCount + inserts.length);
|
||||
|
||||
const { data: data2 } = await em.repo(items).findMany({ offset: oldCount });
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { afterAll, expect as bunExpect, describe, test } from "bun:test";
|
||||
import { stripMark } from "../../src/core/utils";
|
||||
import { Entity, EntityManager, PolymorphicRelation, TextField } from "../../src/data";
|
||||
import { stripMark } from "core/utils/schema";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { TextField } from "data/fields";
|
||||
import { PolymorphicRelation } from "data/relations";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
|
||||
@@ -2,19 +2,20 @@ 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,
|
||||
TextField,
|
||||
} from "../../src/data";
|
||||
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
||||
} from "data/relations";
|
||||
import { DummyConnection } from "data/connection/DummyConnection";
|
||||
import {
|
||||
FieldPrototype,
|
||||
type FieldSchema,
|
||||
@@ -32,8 +33,8 @@ import {
|
||||
number,
|
||||
relation,
|
||||
text,
|
||||
} from "../../src/data/prototype";
|
||||
import { MediaField } from "../../src/media/MediaField";
|
||||
} from "data/prototype";
|
||||
import { MediaField } from "media/MediaField";
|
||||
|
||||
describe("prototype", () => {
|
||||
test("...", () => {
|
||||
@@ -101,7 +102,8 @@ describe("prototype", () => {
|
||||
|
||||
type Posts = Schema<typeof posts2>;
|
||||
|
||||
expect(posts1.toJSON()).toEqual(posts2.toJSON());
|
||||
// @todo: check
|
||||
//expect(posts1.toJSON()).toEqual(posts2.toJSON());
|
||||
});
|
||||
|
||||
test("test example", async () => {
|
||||
@@ -295,9 +297,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,13 +1,14 @@
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { Entity, EntityManager, TextField } from "../../src/data";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { TextField } from "data/fields";
|
||||
import {
|
||||
ManyToManyRelation,
|
||||
ManyToOneRelation,
|
||||
OneToOneRelation,
|
||||
PolymorphicRelation,
|
||||
RelationField,
|
||||
} from "../../src/data/relations";
|
||||
} from "data/relations";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
@@ -77,7 +78,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);
|
||||
@@ -89,11 +90,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");
|
||||
@@ -191,7 +192,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);
|
||||
@@ -202,8 +203,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);
|
||||
@@ -323,7 +324,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,
|
||||
@@ -334,7 +335,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, NumberField, TextField } from "data";
|
||||
import * as p from "data/prototype";
|
||||
import { Entity } from "data/entities";
|
||||
import { NumberField, TextField } from "data/fields";
|
||||
|
||||
describe("[data] Entity", async () => {
|
||||
const entity = new Entity("test", [
|
||||
@@ -47,8 +47,4 @@ describe("[data] Entity", async () => {
|
||||
entity.addField(field);
|
||||
expect(entity.getField("new_field")).toBe(field);
|
||||
});
|
||||
|
||||
test.only("types", async () => {
|
||||
console.log(entity.toTypes());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
ManyToManyRelation,
|
||||
ManyToOneRelation,
|
||||
SchemaManager,
|
||||
} from "../../../src/data";
|
||||
import { UnableToConnectException } from "../../../src/data/errors";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { ManyToManyRelation, ManyToOneRelation } from "data/relations";
|
||||
import { SchemaManager } from "data/schema/SchemaManager";
|
||||
import { UnableToConnectException } from "data/errors";
|
||||
import { getDummyConnection } from "../helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
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 { 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 { getDummyConnection } from "../helper";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(() => disableConsoleLog());
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
afterAll(afterAllCleanup);
|
||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||
|
||||
describe("[data] JoinBuilder", async () => {
|
||||
test("missing relation", async () => {
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
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,
|
||||
type RelationField,
|
||||
RelationField,
|
||||
RelationMutator,
|
||||
TextField,
|
||||
} from "../../../src/data";
|
||||
import * as proto from "../../../src/data/prototype";
|
||||
import { getDummyConnection } from "../helper";
|
||||
} 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";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
afterAll(afterAllCleanup);
|
||||
|
||||
beforeAll(() => disableConsoleLog());
|
||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||
|
||||
describe("[data] Mutator (base)", async () => {
|
||||
const entity = new Entity("items", [
|
||||
new TextField("label", { required: true }),
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import type { Kysely, Transaction } from "kysely";
|
||||
import { Perf } from "core/utils";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
LibsqlConnection,
|
||||
ManyToOneRelation,
|
||||
RepositoryEvents,
|
||||
TextField,
|
||||
entity as $entity,
|
||||
text as $text,
|
||||
em as $em,
|
||||
} from "data";
|
||||
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 { getDummyConnection } from "../helper";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
type E = Kysely<any> | Transaction<any>;
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
afterAll(afterAllCleanup);
|
||||
beforeAll(() => disableConsoleLog());
|
||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||
|
||||
async function sleep(ms: number) {
|
||||
return new Promise((resolve) => {
|
||||
@@ -26,120 +21,6 @@ async function sleep(ms: number) {
|
||||
}
|
||||
|
||||
describe("[Repository]", async () => {
|
||||
test.skip("bulk", async () => {
|
||||
//const connection = dummyConnection;
|
||||
//const connection = getLocalLibsqlConnection();
|
||||
const credentials = null as any; // @todo: determine what to do here
|
||||
const connection = new LibsqlConnection(credentials);
|
||||
|
||||
const em = new EntityManager([], connection);
|
||||
/*const emLibsql = new EntityManager([], {
|
||||
url: connection.url.replace("https", "libsql"),
|
||||
authToken: connection.authToken,
|
||||
});*/
|
||||
const table = "posts";
|
||||
|
||||
const client = connection.getClient();
|
||||
if (!client) {
|
||||
console.log("Cannot perform test without libsql connection");
|
||||
return;
|
||||
}
|
||||
|
||||
const conn = em.connection.kysely;
|
||||
const selectQ = (e: E) => e.selectFrom(table).selectAll().limit(2);
|
||||
const countQ = (e: E) => e.selectFrom(table).select(e.fn.count("*").as("count"));
|
||||
|
||||
async function executeTransaction(em: EntityManager<any>) {
|
||||
return await em.connection.kysely.transaction().execute(async (e) => {
|
||||
const res = await selectQ(e).execute();
|
||||
const count = await countQ(e).execute();
|
||||
|
||||
return [res, count];
|
||||
});
|
||||
}
|
||||
|
||||
async function executeBatch(em: EntityManager<any>) {
|
||||
const queries = [selectQ(conn), countQ(conn)];
|
||||
return await em.connection.batchQuery(queries);
|
||||
}
|
||||
|
||||
async function executeSingleKysely(em: EntityManager<any>) {
|
||||
const res = await selectQ(conn).execute();
|
||||
const count = await countQ(conn).execute();
|
||||
return [res, count];
|
||||
}
|
||||
|
||||
async function executeSingleClient(em: EntityManager<any>) {
|
||||
const q1 = selectQ(conn).compile();
|
||||
const res = await client.execute({
|
||||
sql: q1.sql,
|
||||
args: q1.parameters as any,
|
||||
});
|
||||
|
||||
const q2 = countQ(conn).compile();
|
||||
const count = await client.execute({
|
||||
sql: q2.sql,
|
||||
args: q2.parameters as any,
|
||||
});
|
||||
return [res, count];
|
||||
}
|
||||
|
||||
const transaction = await executeTransaction(em);
|
||||
const batch = await executeBatch(em);
|
||||
|
||||
expect(batch).toEqual(transaction as any);
|
||||
|
||||
const testperf = false;
|
||||
if (testperf) {
|
||||
const times = 5;
|
||||
|
||||
const exec = async (
|
||||
name: string,
|
||||
fn: (em: EntityManager<any>) => Promise<any>,
|
||||
em: EntityManager<any>,
|
||||
) => {
|
||||
const res = await Perf.execute(() => fn(em), times);
|
||||
await sleep(1000);
|
||||
const info = {
|
||||
name,
|
||||
total: res.total.toFixed(2),
|
||||
avg: (res.total / times).toFixed(2),
|
||||
first: res.marks[0].time.toFixed(2),
|
||||
last: res.marks[res.marks.length - 1].time.toFixed(2),
|
||||
};
|
||||
console.log(info.name, info, res.marks);
|
||||
return info;
|
||||
};
|
||||
|
||||
const data: any[] = [];
|
||||
data.push(await exec("transaction.http", executeTransaction, em));
|
||||
data.push(await exec("bulk.http", executeBatch, em));
|
||||
data.push(await exec("singleKy.http", executeSingleKysely, em));
|
||||
data.push(await exec("singleCl.http", executeSingleClient, em));
|
||||
|
||||
/*data.push(await exec("transaction.libsql", executeTransaction, emLibsql));
|
||||
data.push(await exec("bulk.libsql", executeBatch, emLibsql));
|
||||
data.push(await exec("singleKy.libsql", executeSingleKysely, emLibsql));
|
||||
data.push(await exec("singleCl.libsql", executeSingleClient, emLibsql));*/
|
||||
|
||||
console.table(data);
|
||||
/**
|
||||
* ┌───┬────────────────────┬────────┬────────┬────────┬────────┐
|
||||
* │ │ name │ total │ avg │ first │ last │
|
||||
* ├───┼────────────────────┼────────┼────────┼────────┼────────┤
|
||||
* │ 0 │ transaction.http │ 681.29 │ 136.26 │ 136.46 │ 396.09 │
|
||||
* │ 1 │ bulk.http │ 164.82 │ 32.96 │ 32.95 │ 99.91 │
|
||||
* │ 2 │ singleKy.http │ 330.01 │ 66.00 │ 65.86 │ 195.41 │
|
||||
* │ 3 │ singleCl.http │ 326.17 │ 65.23 │ 61.32 │ 198.08 │
|
||||
* │ 4 │ transaction.libsql │ 856.79 │ 171.36 │ 132.31 │ 595.24 │
|
||||
* │ 5 │ bulk.libsql │ 180.63 │ 36.13 │ 35.39 │ 107.71 │
|
||||
* │ 6 │ singleKy.libsql │ 347.11 │ 69.42 │ 65.00 │ 207.14 │
|
||||
* │ 7 │ singleCl.libsql │ 328.60 │ 65.72 │ 62.19 │ 195.04 │
|
||||
* └───┴────────────────────┴────────┴────────┴────────┴────────┘
|
||||
*/
|
||||
}
|
||||
});
|
||||
|
||||
test("count & exists", async () => {
|
||||
const items = new Entity("items", [new TextField("label")]);
|
||||
const em = new EntityManager([items], dummyConnection);
|
||||
@@ -160,25 +41,44 @@ describe("[Repository]", async () => {
|
||||
// count all
|
||||
const res = await em.repository(items).count();
|
||||
expect(res.sql).toBe('select count(*) as "count" from "items"');
|
||||
expect(res.count).toBe(3);
|
||||
expect(res.data.count).toBe(3);
|
||||
|
||||
//
|
||||
{
|
||||
const res = await em.repository(items).findMany();
|
||||
expect(res.count).toBe(3);
|
||||
}
|
||||
|
||||
{
|
||||
const res = await em
|
||||
.repository(items, {
|
||||
includeCounts: true,
|
||||
})
|
||||
.findMany();
|
||||
expect(res.count).toBe(3);
|
||||
}
|
||||
|
||||
// count filtered
|
||||
const res2 = await em.repository(items).count({ label: { $in: ["a", "b"] } });
|
||||
const res2 = await em
|
||||
.repository(items, {
|
||||
includeCounts: true,
|
||||
})
|
||||
.count({ label: { $in: ["a", "b"] } });
|
||||
|
||||
expect(res2.sql).toBe('select count(*) as "count" from "items" where "label" in (?, ?)');
|
||||
expect(res2.parameters).toEqual(["a", "b"]);
|
||||
expect(res2.count).toBe(2);
|
||||
expect(res2.data.count).toBe(2);
|
||||
|
||||
// check exists
|
||||
const res3 = await em.repository(items).exists({ label: "a" });
|
||||
expect(res3.exists).toBe(true);
|
||||
expect(res3.data.exists).toBe(true);
|
||||
|
||||
const res4 = await em.repository(items).exists({ label: "d" });
|
||||
expect(res4.exists).toBe(false);
|
||||
expect(res4.data.exists).toBe(false);
|
||||
|
||||
// for now, allow empty filter
|
||||
const res5 = await em.repository(items).exists({});
|
||||
expect(res5.exists).toBe(true);
|
||||
expect(res5.data.exists).toBe(true);
|
||||
});
|
||||
|
||||
test("option: silent", async () => {
|
||||
@@ -191,6 +91,9 @@ describe("[Repository]", async () => {
|
||||
// should throw because table doesn't exist
|
||||
expect(em.repo("items").findMany({})).rejects.toThrow(/no such table/);
|
||||
// should silently return empty result
|
||||
em.repo("items", { silent: true })
|
||||
.findMany({})
|
||||
.then((r) => r.data);
|
||||
expect(
|
||||
em
|
||||
.repo("items", { silent: true })
|
||||
@@ -209,16 +112,16 @@ describe("[Repository]", async () => {
|
||||
|
||||
expect(
|
||||
em
|
||||
.repo("items")
|
||||
.repo("items", { includeCounts: true })
|
||||
.findMany({})
|
||||
.then((r) => [r.meta.count, r.meta.total]),
|
||||
.then((r) => [r.count, r.total]),
|
||||
).resolves.toEqual([0, 0]);
|
||||
|
||||
expect(
|
||||
em
|
||||
.repo("items", { includeCounts: false })
|
||||
.findMany({})
|
||||
.then((r) => [r.meta.count, r.meta.total]),
|
||||
.then((r) => [r.count, r.total]),
|
||||
).resolves.toEqual([undefined, undefined]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { randomString } from "../../../src/core/utils";
|
||||
import { Entity, EntityIndex, EntityManager, SchemaManager, TextField } from "../../../src/data";
|
||||
import { randomString } from "core/utils";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { TextField, EntityIndex } from "data/fields";
|
||||
import { SchemaManager } from "data/schema/SchemaManager";
|
||||
import { getDummyConnection } from "../helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { getDummyConnection } from "../helper";
|
||||
import { type WhereQuery, WhereBuilder } from "data";
|
||||
import { WhereBuilder, type WhereQuery } from "data/entities/query/WhereBuilder";
|
||||
|
||||
function qb() {
|
||||
const c = getDummyConnection();
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
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 { 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 { schemaToEm } from "../../helper";
|
||||
import { getDummyConnection } from "../helper";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(() => disableConsoleLog());
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
|
||||
@@ -89,9 +88,9 @@ describe("[data] WithBuilder", async () => {
|
||||
const res2 = qb2.compile();
|
||||
|
||||
expect(res2.sql).toBe(
|
||||
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "users"."id" as "id", "users"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" order by "users"."id" asc limit ? offset ?) as obj) as "author" from "posts"',
|
||||
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "users"."id" as "id", "users"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" order by "users"."id" asc limit ?) as obj) as "author" from "posts"',
|
||||
);
|
||||
expect(res2.parameters).toEqual([1, 0]);
|
||||
expect(res2.parameters).toEqual([1]);
|
||||
});
|
||||
|
||||
test("test with empty join", async () => {
|
||||
@@ -194,9 +193,9 @@ describe("[data] WithBuilder", async () => {
|
||||
);
|
||||
const res = qb.compile();
|
||||
expect(res.sql).toBe(
|
||||
'select (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as obj) as "single" from "categories"',
|
||||
'select (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ?) as obj) as "single" from "categories"',
|
||||
);
|
||||
expect(res.parameters).toEqual(["categories.single", 1, 0]);
|
||||
expect(res.parameters).toEqual(["categories.single", 1]);
|
||||
|
||||
const qb2 = WithBuilder.addClause(
|
||||
em,
|
||||
@@ -273,9 +272,9 @@ describe("[data] WithBuilder", async () => {
|
||||
|
||||
//prettyPrintQb(qb);
|
||||
expect(qb.compile().sql).toBe(
|
||||
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username", \'avatar\', "obj"."avatar") from (select "users"."id" as "id", "users"."username" as "username", (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "users"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as obj) as "avatar" from "users" as "users" where "users"."id" = "posts"."users_id" order by "users"."username" asc limit ? offset ?) as obj) as "users" from "posts"',
|
||||
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username", \'avatar\', "obj"."avatar") from (select "users"."id" as "id", "users"."username" as "username", (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "users"."id" = "media"."entity_id" order by "media"."id" asc limit ?) as obj) as "avatar" from "users" as "users" where "users"."id" = "posts"."users_id" order by "users"."username" asc limit ?) as obj) as "users" from "posts"',
|
||||
);
|
||||
expect(qb.compile().parameters).toEqual(["users.avatar", 1, 0, 1, 0]);
|
||||
expect(qb.compile().parameters).toEqual(["users.avatar", 1, 1]);
|
||||
});
|
||||
|
||||
test("compiles with many", async () => {
|
||||
@@ -315,9 +314,9 @@ describe("[data] WithBuilder", async () => {
|
||||
);
|
||||
|
||||
expect(qb.compile().sql).toBe(
|
||||
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'posts_id\', "agg"."posts_id", \'users_id\', "agg"."users_id", \'users\', "agg"."users")), \'[]\') from (select "comments"."id" as "id", "comments"."posts_id" as "posts_id", "comments"."users_id" as "users_id", (select json_object(\'username\', "obj"."username") from (select "users"."username" as "username" from "users" as "users" where "users"."id" = "comments"."users_id" order by "users"."id" asc limit ? offset ?) as obj) as "users" from "comments" as "comments" where "comments"."posts_id" = "posts"."id" order by "comments"."id" asc limit ? offset ?) as agg) as "comments" from "posts"',
|
||||
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'posts_id\', "agg"."posts_id", \'users_id\', "agg"."users_id", \'users\', "agg"."users")), \'[]\') from (select "comments"."id" as "id", "comments"."posts_id" as "posts_id", "comments"."users_id" as "users_id", (select json_object(\'username\', "obj"."username") from (select "users"."username" as "username" from "users" as "users" where "users"."id" = "comments"."users_id" order by "users"."id" asc limit ?) as obj) as "users" from "comments" as "comments" where "comments"."posts_id" = "posts"."id" order by "comments"."id" asc limit ? offset ?) as agg) as "comments" from "posts"',
|
||||
);
|
||||
expect(qb.compile().parameters).toEqual([1, 0, 12, 0]);
|
||||
expect(qb.compile().parameters).toEqual([1, 12, 0]);
|
||||
});
|
||||
|
||||
test("returns correct result", async () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { EntityManager } from "../../../../src/data";
|
||||
import { EntityManager } from "data/entities/EntityManager";
|
||||
import { getDummyConnection } from "../../helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { BooleanField } from "../../../../src/data";
|
||||
import { BooleanField } from "data/fields";
|
||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||
|
||||
describe("[data] BooleanField", async () => {
|
||||
fieldTestSuite({ expect, test }, BooleanField, { defaultValue: true, schemaType: "boolean" });
|
||||
fieldTestSuite(bunTestRunner, BooleanField, { defaultValue: true, schemaType: "boolean" });
|
||||
|
||||
test("transformRetrieve", async () => {
|
||||
const field = new BooleanField("test");
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { DateField } from "../../../../src/data";
|
||||
import { describe, test } from "bun:test";
|
||||
import { DateField } from "data/fields";
|
||||
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
|
||||
describe("[data] DateField", async () => {
|
||||
fieldTestSuite({ expect, test }, DateField, { defaultValue: new Date(), schemaType: "date" });
|
||||
fieldTestSuite(
|
||||
bunTestRunner,
|
||||
DateField,
|
||||
{ defaultValue: new Date(), schemaType: "date" },
|
||||
{ type: "date" },
|
||||
);
|
||||
|
||||
// @todo: add datefield tests
|
||||
test("week", async () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { EnumField } from "../../../../src/data";
|
||||
import { EnumField } from "data/fields";
|
||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||
|
||||
function options(strings: string[]) {
|
||||
@@ -8,7 +9,7 @@ function options(strings: string[]) {
|
||||
|
||||
describe("[data] EnumField", async () => {
|
||||
fieldTestSuite(
|
||||
{ expect, test },
|
||||
bunTestRunner,
|
||||
// @ts-ignore
|
||||
EnumField,
|
||||
{ defaultValue: "a", schemaType: "text" },
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Default, stripMark } from "../../../../src/core/utils";
|
||||
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";
|
||||
|
||||
describe("[data] Field", async () => {
|
||||
class FieldSpec extends Field {
|
||||
@@ -19,10 +20,10 @@ describe("[data] Field", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
fieldTestSuite({ expect, test }, FieldSpec, { defaultValue: "test", schemaType: "text" });
|
||||
fieldTestSuite(bunTestRunner, FieldSpec, { defaultValue: "test", schemaType: "text" });
|
||||
|
||||
test("default config", async () => {
|
||||
const config = Default(baseFieldConfigSchema, {});
|
||||
const config = baseFieldConfigSchema.template({});
|
||||
expect(stripMark(new FieldSpec("test").config)).toEqual(config as any);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { Entity, EntityIndex, Field } from "../../../../src/data";
|
||||
import { Entity } from "data/entities";
|
||||
import { Field, EntityIndex } from "data/fields";
|
||||
import { s } from "core/utils/schema";
|
||||
|
||||
class TestField extends Field {
|
||||
protected getSchema(): any {
|
||||
return Type.Any();
|
||||
return s.any();
|
||||
}
|
||||
|
||||
override schema() {
|
||||
@@ -22,11 +23,4 @@ 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,10 +1,11 @@
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { JsonField } from "../../../../src/data";
|
||||
import { JsonField } from "data/fields";
|
||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||
|
||||
describe("[data] JsonField", async () => {
|
||||
const field = new JsonField("test");
|
||||
fieldTestSuite({ expect, test }, JsonField, {
|
||||
fieldTestSuite(bunTestRunner, 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 "../../../../src/data";
|
||||
import { JsonSchemaField } from "data/fields";
|
||||
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||
|
||||
describe("[data] JsonSchemaField", async () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { NumberField } from "../../../../src/data";
|
||||
import { NumberField } from "data/fields";
|
||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||
|
||||
describe("[data] NumberField", async () => {
|
||||
@@ -15,5 +16,5 @@ describe("[data] NumberField", async () => {
|
||||
expect(transformPersist(field2, 10000)).resolves.toBe(10000);
|
||||
});
|
||||
|
||||
fieldTestSuite({ expect, test }, NumberField, { defaultValue: 12, schemaType: "integer" });
|
||||
fieldTestSuite(bunTestRunner, NumberField, { defaultValue: 12, schemaType: "integer" });
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { PrimaryField } from "../../../../src/data";
|
||||
import { PrimaryField } from "data/fields";
|
||||
|
||||
describe("[data] PrimaryField", async () => {
|
||||
const field = new PrimaryField("primary");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { TextField } from "../../../../src/data";
|
||||
import { TextField } from "data/fields";
|
||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
|
||||
describe("[data] TextField", async () => {
|
||||
test("transformPersist (config)", async () => {
|
||||
@@ -11,5 +12,5 @@ describe("[data] TextField", async () => {
|
||||
expect(transformPersist(field, "abc")).resolves.toBe("abc");
|
||||
});
|
||||
|
||||
fieldTestSuite({ expect, test }, TextField, { defaultValue: "abc", schemaType: "text" });
|
||||
fieldTestSuite(bunTestRunner, TextField, { defaultValue: "abc", schemaType: "text" });
|
||||
});
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { describe, expect, it, test } from "bun:test";
|
||||
import { Entity, type EntityManager } from "../../../../src/data";
|
||||
import { Entity, type EntityManager } from "data/entities";
|
||||
import {
|
||||
type BaseRelationConfig,
|
||||
EntityRelation,
|
||||
EntityRelationAnchor,
|
||||
ManyToManyRelation,
|
||||
RelationTypes,
|
||||
} from "../../../../src/data/relations";
|
||||
} from "data/relations";
|
||||
import * as proto from "data/prototype";
|
||||
|
||||
class TestEntityRelation extends EntityRelation {
|
||||
constructor(config?: BaseRelationConfig) {
|
||||
@@ -24,11 +26,11 @@ class TestEntityRelation extends EntityRelation {
|
||||
return this;
|
||||
}
|
||||
|
||||
buildWith(a: any, b: any, c: any): any {
|
||||
buildWith(): any {
|
||||
return;
|
||||
}
|
||||
|
||||
buildJoin(a: any, b: any): any {
|
||||
buildJoin(): any {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -75,4 +77,15 @@ 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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
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));
|
||||
});
|
||||
});
|
||||
@@ -41,7 +41,7 @@ beforeAll(() =>
|
||||
);
|
||||
afterAll(unmockFetch);
|
||||
|
||||
describe("FetchTask", async () => {
|
||||
describe.skip("FetchTask", async () => {
|
||||
test("Simple fetch", async () => {
|
||||
const task = new FetchTask("Fetch Something", {
|
||||
url: "https://jsonplaceholder.typicode.com/todos/1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Flow, LogTask, SubFlowTask, RenderTask, Task } from "../../src/flows";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { s } from "core/utils/schema";
|
||||
|
||||
export class StringifyTask<Output extends string> extends Task<
|
||||
typeof StringifyTask.schema,
|
||||
@@ -8,18 +8,16 @@ export class StringifyTask<Output extends string> extends Task<
|
||||
> {
|
||||
type = "stringify";
|
||||
|
||||
static override schema = Type.Optional(
|
||||
Type.Object({
|
||||
input: Type.Optional(Type.String()),
|
||||
}),
|
||||
);
|
||||
static override schema = s.object({
|
||||
input: s.string().optional(),
|
||||
});
|
||||
|
||||
async execute() {
|
||||
return JSON.stringify(this.params.input) as Output;
|
||||
}
|
||||
}
|
||||
|
||||
describe("SubFlowTask", async () => {
|
||||
describe.skip("SubFlowTask", async () => {
|
||||
test("Simple Subflow", async () => {
|
||||
const subTask = new RenderTask("render", {
|
||||
render: "subflow",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { Task } from "../../src/flows";
|
||||
import { dynamic } from "../../src/flows/tasks/Task";
|
||||
import { s } from "core/utils/schema";
|
||||
|
||||
describe("Task", async () => {
|
||||
describe.skip("Task", async () => {
|
||||
test("resolveParams: template with parse", async () => {
|
||||
const result = await Task.resolveParams(
|
||||
Type.Object({ test: dynamic(Type.Number()) }),
|
||||
s.object({ test: dynamic(s.number()) }),
|
||||
{
|
||||
test: "{{ some.path }}",
|
||||
},
|
||||
@@ -22,7 +22,7 @@ describe("Task", async () => {
|
||||
|
||||
test("resolveParams: with string", async () => {
|
||||
const result = await Task.resolveParams(
|
||||
Type.Object({ test: Type.String() }),
|
||||
s.object({ test: s.string() }),
|
||||
{
|
||||
test: "{{ some.path }}",
|
||||
},
|
||||
@@ -38,7 +38,7 @@ describe("Task", async () => {
|
||||
|
||||
test("resolveParams: with object", async () => {
|
||||
const result = await Task.resolveParams(
|
||||
Type.Object({ test: dynamic(Type.Object({ key: Type.String(), value: Type.String() })) }),
|
||||
s.object({ test: dynamic(s.object({ key: s.string(), value: s.string() })) }),
|
||||
{
|
||||
test: { key: "path", value: "{{ some.path }}" },
|
||||
},
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Hono } from "hono";
|
||||
import { Event, EventManager } from "../../src/core/events";
|
||||
import { parse } from "../../src/core/utils";
|
||||
import { type Static, type StaticDecode, Type } from "@sinclair/typebox";
|
||||
import { s, parse } from "core/utils/schema";
|
||||
import { EventTrigger, Flow, HttpTrigger, type InputsMap, Task } from "../../src/flows";
|
||||
import { dynamic } from "../../src/flows/tasks/Task";
|
||||
|
||||
@@ -15,15 +14,15 @@ class Passthrough extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
type OutputIn = Static<typeof OutputParamTask.schema>;
|
||||
type OutputOut = StaticDecode<typeof OutputParamTask.schema>;
|
||||
type OutputIn = s.Static<typeof OutputParamTask.schema>;
|
||||
type OutputOut = s.StaticCoerced<typeof OutputParamTask.schema>;
|
||||
|
||||
class OutputParamTask extends Task<typeof OutputParamTask.schema> {
|
||||
type = "output-param";
|
||||
|
||||
static override schema = Type.Object({
|
||||
static override schema = s.strictObject({
|
||||
number: dynamic(
|
||||
Type.Number({
|
||||
s.number({
|
||||
title: "Output number",
|
||||
}),
|
||||
Number.parseInt,
|
||||
@@ -44,7 +43,7 @@ class PassthroughFlowInput extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
describe("Flow task inputs", async () => {
|
||||
describe.skip("Flow task inputs", async () => {
|
||||
test("types", async () => {
|
||||
const schema = OutputParamTask.schema;
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class ExecTask extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
describe("Flow trigger", async () => {
|
||||
describe.skip("Flow trigger", async () => {
|
||||
test("manual trigger", async () => {
|
||||
let called = false;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { isEqual } from "lodash-es";
|
||||
import { _jsonp, withDisabledConsole } from "../../src/core/utils";
|
||||
import { type Static, Type } from "@sinclair/typebox";
|
||||
import { s } from "core/utils/schema";
|
||||
import { Condition, ExecutionEvent, FetchTask, Flow, LogTask, Task } from "../../src/flows";
|
||||
|
||||
/*beforeAll(disableConsoleLog);
|
||||
@@ -11,19 +11,19 @@ afterAll(enableConsoleLog);*/
|
||||
class ExecTask extends Task<typeof ExecTask.schema> {
|
||||
type = "exec";
|
||||
|
||||
static override schema = Type.Object({
|
||||
delay: Type.Number({ default: 10 }),
|
||||
static override schema = s.object({
|
||||
delay: s.number({ default: 10 }),
|
||||
});
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
params: Static<typeof ExecTask.schema>,
|
||||
params: s.Static<typeof ExecTask.schema>,
|
||||
private func: () => Promise<any>,
|
||||
) {
|
||||
super(name, params);
|
||||
}
|
||||
|
||||
override clone(name: string, params: Static<typeof ExecTask.schema>) {
|
||||
override clone(name: string, params: s.Static<typeof ExecTask.schema>) {
|
||||
return new ExecTask(name, params, this.func);
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ function getObjectDiff(obj1, obj2) {
|
||||
return diff;
|
||||
}
|
||||
|
||||
describe("Flow tests", async () => {
|
||||
describe.skip("Flow tests", async () => {
|
||||
test("Simple single task", async () => {
|
||||
const simple = getTask(0);
|
||||
|
||||
|
||||
+4
-22
@@ -2,11 +2,12 @@ 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 "core/utils/strings";
|
||||
import { slugify } from "bknd/utils";
|
||||
import { type Connection, SqliteLocalConnection } from "data/connection";
|
||||
import { EntityManager } from "data/entities/EntityManager";
|
||||
|
||||
export function getDummyDatabase(memory: boolean = true): {
|
||||
dummyDb: SqliteDatabase;
|
||||
@@ -38,25 +39,6 @@ export function getLocalLibsqlConnection() {
|
||||
return { url: "http://127.0.0.1:8080" };
|
||||
}
|
||||
|
||||
type ConsoleSeverity = "log" | "warn" | "error";
|
||||
const _oldConsoles = {
|
||||
log: console.log,
|
||||
warn: console.warn,
|
||||
error: console.error,
|
||||
};
|
||||
|
||||
export function disableConsoleLog(severities: ConsoleSeverity[] = ["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 };
|
||||
@@ -64,7 +46,7 @@ export function compileQb(qb: SelectQueryBuilder<any, any, any>) {
|
||||
|
||||
export function prettyPrintQb(qb: SelectQueryBuilder<any, any, any>) {
|
||||
const { sql, parameters } = qb.compile();
|
||||
console.log("$", sqlFormat(sql), "\n[params]", parameters);
|
||||
console.info("$", sqlFormat(sql), "\n[params]", parameters);
|
||||
}
|
||||
|
||||
export function schemaToEm(s: ReturnType<typeof protoEm>, conn?: Connection): EntityManager<any> {
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from "bun:test";
|
||||
import { App, createApp } from "../../src";
|
||||
import type { AuthResponse } from "../../src/auth";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||
import { App, createApp, type AuthResponse } from "../../src";
|
||||
import { auth } from "../../src/auth/middlewares";
|
||||
import { randomString, secureRandomString, withDisabledConsole } from "../../src/core/utils";
|
||||
import { disableConsoleLog, enableConsoleLog, getDummyConnection } from "../helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
afterEach(afterAllCleanup);
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
import { getDummyConnection } from "../helper";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
@@ -66,9 +63,10 @@ const configs = {
|
||||
};
|
||||
|
||||
function createAuthApp() {
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
const app = createApp({
|
||||
connection: dummyConnection,
|
||||
initialConfig: {
|
||||
config: {
|
||||
auth: configs.auth,
|
||||
},
|
||||
});
|
||||
@@ -151,8 +149,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 () => {
|
||||
@@ -223,7 +221,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) => {
|
||||
@@ -242,7 +240,7 @@ describe("integration auth", () => {
|
||||
{
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
const res = await app.server.request("/get");
|
||||
const data = await res.json();
|
||||
const data = (await res.json()) as any;
|
||||
expect(data.user).toBe(null);
|
||||
expect(await $fns.me()).toEqual({ user: null as any });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { createApp } from "../../src";
|
||||
import { afterAll, beforeAll, 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 () => {
|
||||
@@ -13,9 +17,8 @@ describe("integration config", () => {
|
||||
|
||||
// create entity
|
||||
await api.system.addConfig("data", "entities.posts", {
|
||||
name: "posts",
|
||||
config: { sort_field: "id", sort_dir: "asc" },
|
||||
fields: { id: { type: "primary", name: "id" }, asdf: { type: "text" } },
|
||||
fields: { id: { type: "primary" }, asdf: { type: "text" } },
|
||||
type: "regular",
|
||||
});
|
||||
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
/// <reference types="@types/bun" />
|
||||
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { createApp, registries } from "../../src";
|
||||
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, disableConsoleLog, enableConsoleLog } from "../helper";
|
||||
import { assetsPath, assetsTmpPath } from "../helper";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(() => {
|
||||
disableConsoleLog();
|
||||
registries.media.register("local", StorageLocalAdapter);
|
||||
});
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
const path = `${assetsPath}/image.png`;
|
||||
|
||||
async function makeApp(mediaOverride: Partial<TAppMediaConfig> = {}) {
|
||||
const app = createApp({
|
||||
initialConfig: {
|
||||
config: {
|
||||
media: mergeObject(
|
||||
{
|
||||
enabled: true,
|
||||
@@ -39,13 +43,9 @@ function makeName(ext: string) {
|
||||
return randomString(10) + "." + ext;
|
||||
}
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("MediaController", () => {
|
||||
test("accepts direct", async () => {
|
||||
const app = await makeApp();
|
||||
console.log("app", app);
|
||||
|
||||
const file = Bun.file(path);
|
||||
const name = makeName("png");
|
||||
@@ -54,7 +54,6 @@ 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";
|
||||
import { StorageAdapter } from "media/storage/StorageAdapter";
|
||||
|
||||
class TestAdapter extends StorageAdapter {
|
||||
files: Record<string, FileBody> = {};
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
||||
import { createApp } from "../../src";
|
||||
import { createApp } from "core/test/utils";
|
||||
import { AuthController } from "../../src/auth/api/AuthController";
|
||||
import { em, entity, make, text } from "../../src/data";
|
||||
import { AppAuth, type ModuleBuildContext } from "../../src/modules";
|
||||
import { disableConsoleLog, enableConsoleLog } from "../helper";
|
||||
// @ts-ignore
|
||||
import { em, entity, make, text } from "data/prototype";
|
||||
import { AppAuth, type ModuleBuildContext } from "modules";
|
||||
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("AppAuth", () => {
|
||||
test.skip("...", () => {
|
||||
const auth = new AppAuth({});
|
||||
console.log(auth.toJSON());
|
||||
console.log(auth.config);
|
||||
});
|
||||
|
||||
moduleTestSuite(AppAuth);
|
||||
|
||||
let ctx: ModuleBuildContext;
|
||||
@@ -142,7 +150,7 @@ describe("AppAuth", () => {
|
||||
|
||||
test("registers auth middleware for bknd routes only", async () => {
|
||||
const app = createApp({
|
||||
initialConfig: {
|
||||
config: {
|
||||
auth: {
|
||||
enabled: true,
|
||||
jwt: {
|
||||
@@ -172,7 +180,7 @@ describe("AppAuth", () => {
|
||||
|
||||
test("should allow additional user fields", async () => {
|
||||
const app = createApp({
|
||||
initialConfig: {
|
||||
config: {
|
||||
auth: {
|
||||
entity_name: "users",
|
||||
enabled: true,
|
||||
@@ -196,7 +204,7 @@ describe("AppAuth", () => {
|
||||
|
||||
test("ensure user field configs is always correct", async () => {
|
||||
const app = createApp({
|
||||
initialConfig: {
|
||||
config: {
|
||||
auth: {
|
||||
enabled: true,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { beforeEach, describe, expect, test } from "bun:test";
|
||||
import { parse } from "../../src/core/utils";
|
||||
import { parse } from "core/utils/schema";
|
||||
import { fieldsSchema } from "../../src/data/data-schema";
|
||||
import { AppData, type ModuleBuildContext } from "../../src/modules";
|
||||
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { createApp, registries } from "../../src";
|
||||
import { em, entity, text } from "../../src/data";
|
||||
import { createApp } from "core/test/utils";
|
||||
import { em, entity, text } from "data/prototype";
|
||||
import { registries } from "modules/registries";
|
||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||
import { AppMedia } from "../../src/modules";
|
||||
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);
|
||||
|
||||
const app = createApp({
|
||||
initialConfig: {
|
||||
config: {
|
||||
media: {
|
||||
entity_name: "media",
|
||||
enabled: true,
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
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,12 +1,13 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { stripMark } from "../../src/core/utils";
|
||||
import { type TSchema, Type } from "@sinclair/typebox";
|
||||
import { EntityManager, em, entity, index, text } from "../../src/data";
|
||||
import { s, stripMark } from "core/utils/schema";
|
||||
import { em, entity, index, text } from "data/prototype";
|
||||
import { EntityManager } from "data/entities/EntityManager";
|
||||
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
||||
import { Module } from "../../src/modules/Module";
|
||||
import { ModuleHelper } from "modules/ModuleHelper";
|
||||
|
||||
function createModule<Schema extends TSchema>(schema: Schema) {
|
||||
class TestModule extends Module<typeof schema> {
|
||||
function createModule<Schema extends s.Schema>(schema: Schema) {
|
||||
return class TestModule extends Module<Schema> {
|
||||
getSchema() {
|
||||
return schema;
|
||||
}
|
||||
@@ -16,9 +17,7 @@ function createModule<Schema extends TSchema>(schema: Schema) {
|
||||
override useForceParse() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return TestModule;
|
||||
};
|
||||
}
|
||||
|
||||
describe("Module", async () => {
|
||||
@@ -26,7 +25,7 @@ describe("Module", async () => {
|
||||
test("listener", async () => {
|
||||
let result: any;
|
||||
|
||||
const module = createModule(Type.Object({ a: Type.String() }));
|
||||
const module = createModule(s.object({ a: s.string() }));
|
||||
const m = new module({ a: "test" });
|
||||
|
||||
await m.schema().set({ a: "test2" });
|
||||
@@ -42,13 +41,13 @@ describe("Module", async () => {
|
||||
describe("db schema", () => {
|
||||
class M extends Module {
|
||||
override getSchema() {
|
||||
return Type.Object({});
|
||||
return s.object({});
|
||||
}
|
||||
|
||||
prt = {
|
||||
ensureEntity: this.ensureEntity.bind(this),
|
||||
ensureIndex: this.ensureIndex.bind(this),
|
||||
ensureSchema: this.ensureSchema.bind(this),
|
||||
ensureEntity: this.ctx.helper.ensureEntity.bind(this.ctx.helper),
|
||||
ensureIndex: this.ctx.helper.ensureIndex.bind(this.ctx.helper),
|
||||
ensureSchema: this.ctx.helper.ensureSchema.bind(this.ctx.helper),
|
||||
};
|
||||
|
||||
get em() {
|
||||
@@ -63,7 +62,11 @@ describe("Module", async () => {
|
||||
_em.relations,
|
||||
_em.indices,
|
||||
);
|
||||
return new M({} as any, { em, flags: Module.ctx_flags } as any);
|
||||
const ctx = {
|
||||
em,
|
||||
flags: Module.ctx_flags,
|
||||
};
|
||||
return new M({} as any, { ...ctx, helper: new ModuleHelper(ctx as any) } as any);
|
||||
}
|
||||
function flat(_em: EntityManager) {
|
||||
return {
|
||||
@@ -143,14 +146,9 @@ describe("Module", async () => {
|
||||
|
||||
// this should only add the field "important"
|
||||
m.prt.ensureEntity(
|
||||
entity(
|
||||
"u",
|
||||
{
|
||||
important: text(),
|
||||
},
|
||||
undefined,
|
||||
"system",
|
||||
),
|
||||
entity("u", {
|
||||
important: text(),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(m.ctx.flags.sync_required).toBe(true);
|
||||
@@ -159,8 +157,7 @@ describe("Module", async () => {
|
||||
{
|
||||
name: "u",
|
||||
fields: ["id", "name", "important"],
|
||||
// ensured type must be present
|
||||
type: "system",
|
||||
type: "regular",
|
||||
},
|
||||
{
|
||||
name: "p",
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
||||
import { disableConsoleLog, enableConsoleLog, stripMark } from "core/utils";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { Connection, entity, text } from "data";
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
import { Module } from "modules/Module";
|
||||
import { type ConfigTable, getDefaultConfig, ModuleManager } from "modules/ModuleManager";
|
||||
import { CURRENT_VERSION, TABLE_NAME } from "modules/migrations";
|
||||
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 { getDummyConnection } from "../helper";
|
||||
import { diff } from "core/object/diff";
|
||||
import type { Static } from "@sinclair/typebox";
|
||||
import { s, stripMark } from "core/utils/schema";
|
||||
import { Connection } from "data/connection/Connection";
|
||||
import { entity, text } from "data/prototype";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("ModuleManager", async () => {
|
||||
test("s1: no config, no build", async () => {
|
||||
@@ -92,7 +97,11 @@ describe("ModuleManager", async () => {
|
||||
|
||||
await mm2.build();
|
||||
|
||||
expect(stripMark(json)).toEqual(stripMark(mm2.configs()));
|
||||
/* console.log({
|
||||
json,
|
||||
configs: mm2.configs(),
|
||||
}); */
|
||||
//expect(stripMark(json)).toEqual(stripMark(mm2.configs()));
|
||||
expect(mm2.configs().data.entities?.test).toBeDefined();
|
||||
expect(mm2.configs().data.entities?.test?.fields?.content).toBeDefined();
|
||||
expect(mm2.get("data").toJSON().entities?.test?.fields?.content).toBeDefined();
|
||||
@@ -129,7 +138,7 @@ describe("ModuleManager", async () => {
|
||||
const db = c2.dummyConnection.kysely;
|
||||
|
||||
const mm2 = new ModuleManager(c2.dummyConnection, {
|
||||
initial: { version: version - 1, ...json },
|
||||
initial: { version: version - 1, ...json } as any,
|
||||
});
|
||||
await mm2.syncConfigTable();
|
||||
await db
|
||||
@@ -257,10 +266,10 @@ describe("ModuleManager", async () => {
|
||||
// @todo: add tests for migrations (check "backup" and new version)
|
||||
|
||||
describe("revert", async () => {
|
||||
const failingModuleSchema = Type.Object({
|
||||
value: Type.Optional(Type.Number()),
|
||||
const failingModuleSchema = s.partialObject({
|
||||
value: s.number(),
|
||||
});
|
||||
class FailingModule extends Module<typeof failingModuleSchema> {
|
||||
class FailingModule extends Module<s.Static<typeof failingModuleSchema>> {
|
||||
getSchema() {
|
||||
return failingModuleSchema;
|
||||
}
|
||||
@@ -431,11 +440,11 @@ describe("ModuleManager", async () => {
|
||||
});
|
||||
|
||||
describe("validate & revert", () => {
|
||||
const schema = Type.Object({
|
||||
value: Type.Array(Type.Number(), { default: [] }),
|
||||
const schema = s.object({
|
||||
value: s.array(s.number()),
|
||||
});
|
||||
type SampleSchema = Static<typeof schema>;
|
||||
class Sample extends Module<typeof schema> {
|
||||
type SampleSchema = s.Static<typeof schema>;
|
||||
class Sample extends Module<SampleSchema> {
|
||||
getSchema() {
|
||||
return schema;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { type InitialModuleConfigs, createApp } from "../../../src";
|
||||
|
||||
import { type Kysely, sql } from "kysely";
|
||||
@@ -6,6 +6,10 @@ 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,17 +1,17 @@
|
||||
import { beforeEach, describe, expect, it } from "bun:test";
|
||||
|
||||
import { Hono } from "hono";
|
||||
import { Guard } from "../../src/auth";
|
||||
import { DebugLogger } from "../../src/core";
|
||||
import { EventManager } from "../../src/core/events";
|
||||
import { Default, stripMark } from "../../src/core/utils";
|
||||
import { EntityManager } from "../../src/data";
|
||||
import { Module, type ModuleBuildContext } from "../../src/modules/Module";
|
||||
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 { getDummyConnection } from "../helper";
|
||||
import { ModuleHelper } from "modules/ModuleHelper";
|
||||
import { DebugLogger, McpServer } from "bknd/utils";
|
||||
|
||||
export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildContext {
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
return {
|
||||
const ctx = {
|
||||
connection: dummyConnection,
|
||||
server: new Hono(),
|
||||
em: new EntityManager([], dummyConnection),
|
||||
@@ -19,8 +19,13 @@ export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildCon
|
||||
guard: new Guard(),
|
||||
flags: Module.ctx_flags,
|
||||
logger: new DebugLogger(false),
|
||||
mcp: new McpServer(),
|
||||
...overrides,
|
||||
};
|
||||
return {
|
||||
...ctx,
|
||||
helper: new ModuleHelper(ctx as any),
|
||||
} as any;
|
||||
}
|
||||
|
||||
export function moduleTestSuite(module: { new (): Module }) {
|
||||
@@ -40,7 +45,8 @@ export function moduleTestSuite(module: { new (): Module }) {
|
||||
it("uses the default config", async () => {
|
||||
const m = new module();
|
||||
await m.setContext(ctx).build();
|
||||
expect(stripMark(m.toJSON())).toEqual(Default(m.getSchema(), {}));
|
||||
expect(m.toJSON()).toEqual(m.getSchema().template({}, { withOptional: true }));
|
||||
//expect(stripMark(m.toJSON())).toEqual(Default(m.getSchema(), {}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -102,7 +102,9 @@ describe("json form", () => {
|
||||
] satisfies [string, Exclude<JSONSchema, boolean>, boolean][];
|
||||
|
||||
for (const [pointer, schema, output] of examples) {
|
||||
expect(utils.isRequired(new Draft2019(schema), pointer, schema)).toBe(output);
|
||||
expect(utils.isRequired(new Draft2019(schema), pointer, schema), `${pointer} `).toBe(
|
||||
output,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+25
-1
@@ -1,6 +1,29 @@
|
||||
import pkg from "./package.json" with { type: "json" };
|
||||
import c from "picocolors";
|
||||
import { formatNumber } from "core/utils";
|
||||
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);
|
||||
}
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: ["./src/cli/index.ts"],
|
||||
@@ -8,6 +31,7 @@ const result = await Bun.build({
|
||||
outdir: "./dist/cli",
|
||||
env: "PUBLIC_*",
|
||||
minify: true,
|
||||
external,
|
||||
define: {
|
||||
__isDev: "0",
|
||||
__version: JSON.stringify(pkg.version),
|
||||
|
||||
+106
-59
@@ -1,6 +1,7 @@
|
||||
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");
|
||||
@@ -9,6 +10,14 @@ 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),
|
||||
@@ -27,11 +36,11 @@ function buildTypes() {
|
||||
Bun.spawn(["bun", "build:types"], {
|
||||
stdout: "inherit",
|
||||
onExit: () => {
|
||||
console.info("Types built");
|
||||
oldConsole.log(c.cyan("[Types]"), c.green("built"));
|
||||
Bun.spawn(["bun", "tsc-alias"], {
|
||||
stdout: "inherit",
|
||||
onExit: () => {
|
||||
console.info("Types aliased");
|
||||
oldConsole.log(c.cyan("[Types]"), c.green("aliased"));
|
||||
types_running = false;
|
||||
},
|
||||
});
|
||||
@@ -39,6 +48,10 @@ function buildTypes() {
|
||||
});
|
||||
}
|
||||
|
||||
if (types && !watch) {
|
||||
buildTypes();
|
||||
}
|
||||
|
||||
let watcher_timeout: any;
|
||||
function delayTypes() {
|
||||
if (!watch || !types) return;
|
||||
@@ -48,49 +61,44 @@ function delayTypes() {
|
||||
watcher_timeout = setTimeout(buildTypes, 1000);
|
||||
}
|
||||
|
||||
if (types && !watch) {
|
||||
buildTypes();
|
||||
}
|
||||
|
||||
function banner(title: string) {
|
||||
console.info("");
|
||||
console.info("=".repeat(40));
|
||||
console.info(title.toUpperCase());
|
||||
console.info("-".repeat(40));
|
||||
}
|
||||
const dependencies = Object.keys(pkg.dependencies);
|
||||
|
||||
// collection of always-external packages
|
||||
const external = ["bun:test", "node:test", "node:assert/strict", "@libsql/client"] as const;
|
||||
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/index.ts",
|
||||
"src/core/utils/index.ts",
|
||||
"src/data/index.ts",
|
||||
"src/media/index.ts",
|
||||
],
|
||||
entry: ["src/index.ts", "src/core/utils/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"));
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -134,7 +142,6 @@ 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"],
|
||||
@@ -142,10 +149,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"],
|
||||
@@ -153,6 +160,7 @@ async function buildUi() {
|
||||
onSuccess: async () => {
|
||||
await rewriteClient("./dist/ui/client/index.js");
|
||||
delayTypes();
|
||||
oldConsole.log(c.cyan("[UI]"), "Client", c.green("built"));
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -163,7 +171,6 @@ 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,
|
||||
@@ -197,6 +204,7 @@ async function buildUiElements() {
|
||||
onSuccess: async () => {
|
||||
await rewriteClient("./dist/ui/elements/index.js");
|
||||
delayTypes();
|
||||
oldConsole.log(c.cyan("[UI]"), "Elements", c.green("built"));
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -217,6 +225,7 @@ 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: {
|
||||
@@ -225,51 +234,89 @@ function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsu
|
||||
},
|
||||
external: [
|
||||
/^cloudflare*/,
|
||||
/^@?(hono|libsql).*?/,
|
||||
/^@?hono.*?/,
|
||||
/^(bknd|react|next|node).*?/,
|
||||
/.*\.(html)$/,
|
||||
...external,
|
||||
...(Array.isArray(overrides.external) ? overrides.external : []),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async function buildAdapters() {
|
||||
banner("Building Adapters");
|
||||
// base adapter handles
|
||||
await tsup.build({
|
||||
...baseConfig(""),
|
||||
entry: ["src/adapter/index.ts"],
|
||||
outDir: "dist/adapter",
|
||||
});
|
||||
|
||||
// specific adatpers
|
||||
await tsup.build(baseConfig("react-router"));
|
||||
await tsup.build(baseConfig("bun"));
|
||||
await tsup.build(baseConfig("astro"));
|
||||
await tsup.build(baseConfig("aws"));
|
||||
await tsup.build(
|
||||
baseConfig("cloudflare", {
|
||||
external: [/^kysely/],
|
||||
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,
|
||||
}),
|
||||
);
|
||||
|
||||
await tsup.build({
|
||||
...baseConfig("vite"),
|
||||
platform: "node",
|
||||
});
|
||||
// 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"],
|
||||
}),
|
||||
),
|
||||
|
||||
await tsup.build({
|
||||
...baseConfig("nextjs"),
|
||||
platform: "node",
|
||||
});
|
||||
tsup.build({
|
||||
...baseConfig("vite"),
|
||||
platform: "node",
|
||||
}),
|
||||
|
||||
await tsup.build({
|
||||
...baseConfig("node"),
|
||||
platform: "node",
|
||||
});
|
||||
tsup.build({
|
||||
...baseConfig("nextjs"),
|
||||
platform: "node",
|
||||
}),
|
||||
|
||||
tsup.build({
|
||||
...baseConfig("node"),
|
||||
platform: "node",
|
||||
}),
|
||||
|
||||
tsup.build({
|
||||
...baseConfig("sqlite/edge"),
|
||||
entry: ["src/adapter/sqlite/edge.ts"],
|
||||
outDir: "dist/adapter/sqlite",
|
||||
metafile: false,
|
||||
}),
|
||||
|
||||
tsup.build({
|
||||
...baseConfig("sqlite/node"),
|
||||
entry: ["src/adapter/sqlite/node.ts"],
|
||||
outDir: "dist/adapter/sqlite",
|
||||
platform: "node",
|
||||
metafile: false,
|
||||
}),
|
||||
|
||||
tsup.build({
|
||||
...baseConfig("sqlite/bun"),
|
||||
entry: ["src/adapter/sqlite/bun.ts"],
|
||||
outDir: "dist/adapter/sqlite",
|
||||
metafile: false,
|
||||
external: [/^bun\:.*/],
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
await buildApi();
|
||||
await buildUi();
|
||||
await buildUiElements();
|
||||
await buildAdapters();
|
||||
await Promise.all([buildApi(), buildUi(), buildUiElements(), buildAdapters()]);
|
||||
|
||||
+2
-1
@@ -2,4 +2,5 @@
|
||||
#registry = "http://localhost:4873"
|
||||
|
||||
[test]
|
||||
coverageSkipTestFiles = true
|
||||
coverageSkipTestFiles = true
|
||||
console.depth = 10
|
||||
+6
-6
@@ -17,7 +17,7 @@ async function run(
|
||||
});
|
||||
|
||||
// Read from stdout
|
||||
const reader = proc.stdout.getReader();
|
||||
const reader = (proc.stdout as ReadableStream).getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
// Function to read chunks
|
||||
@@ -30,7 +30,7 @@ async function run(
|
||||
|
||||
const text = decoder.decode(value);
|
||||
if (!resolveCalled) {
|
||||
console.log(c.dim(text.replace(/\n$/, "")));
|
||||
console.info(c.dim(text.replace(/\n$/, "")));
|
||||
}
|
||||
onChunk(
|
||||
text,
|
||||
@@ -189,21 +189,21 @@ const adapters = {
|
||||
|
||||
async function testAdapter(name: keyof typeof adapters) {
|
||||
const config = adapters[name];
|
||||
console.log("adapter", c.cyan(name));
|
||||
console.info("adapter", c.cyan(name));
|
||||
await config.clean();
|
||||
|
||||
const { proc, data } = await config.start();
|
||||
console.log("proc:", proc.pid, "data:", c.cyan(data));
|
||||
console.info("proc:", proc.pid, "data:", c.cyan(data));
|
||||
//proc.kill();process.exit(0);
|
||||
|
||||
const add_env = "env" in config && config.env ? config.env : "";
|
||||
await $`TEST_URL=${data} TEST_ADAPTER=${name} ${add_env} bun run test:e2e`;
|
||||
console.log("DONE!");
|
||||
console.info("DONE!");
|
||||
|
||||
while (!proc.killed) {
|
||||
proc.kill("SIGINT");
|
||||
await Bun.sleep(250);
|
||||
console.log("Waiting for process to exit...");
|
||||
console.info("Waiting for process to exit...");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
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();
|
||||
+66
-34
@@ -3,7 +3,7 @@
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"bin": "./dist/cli/index.js",
|
||||
"version": "0.14.0",
|
||||
"version": "0.18.0-rc.4",
|
||||
"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,6 +13,10 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/bknd-io/bknd/issues"
|
||||
},
|
||||
"packageManager": "bun@1.2.19",
|
||||
"engines": {
|
||||
"node": ">=22.13"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "BKND_CLI_LOG_LEVEL=debug vite",
|
||||
"build": "NODE_ENV=production bun run build.ts --minify --types",
|
||||
@@ -26,22 +30,21 @@
|
||||
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly && tsc-alias",
|
||||
"updater": "bun x npm-check-updates -ui",
|
||||
"cli": "LOCAL=1 bun src/cli/index.ts",
|
||||
"prepublishOnly": "bun run types && bun run test && bun run test:node && bun run test:e2e && bun run build:all && cp ../README.md ./",
|
||||
"prepublishOnly": "bun run types && bun run test && bun run test:node && NODE_NO_WARNINGS=1 VITE_DB_URL=:memory: bun run test:e2e && bun run build:all && cp ../README.md ./",
|
||||
"postpublish": "rm -f README.md",
|
||||
"test": "ALL_TESTS=1 bun test --bail",
|
||||
"test:all": "bun run test && bun run test:node",
|
||||
"test:bun": "ALL_TESTS=1 bun test --bail",
|
||||
"test:node": "tsx --test $(find . -type f -name '*.native-spec.ts')",
|
||||
"test:node": "vitest run",
|
||||
"test:adapters": "bun test src/adapter/**/*.adapter.spec.ts --bail",
|
||||
"test:coverage": "ALL_TESTS=1 bun test --bail --coverage",
|
||||
"test:vitest": "vitest run",
|
||||
"test:vitest:watch": "vitest",
|
||||
"test:vitest:coverage": "vitest run --coverage",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:adapters": "bun run e2e/adapters.ts",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"test:e2e:debug": "playwright test --debug",
|
||||
"test:e2e:report": "playwright show-report"
|
||||
"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"
|
||||
},
|
||||
"license": "FSL-1.1-MIT",
|
||||
"dependencies": {
|
||||
@@ -50,10 +53,8 @@
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@hono/swagger-ui": "^0.5.1",
|
||||
"@libsql/client": "^0.15.2",
|
||||
"@mantine/core": "^7.17.1",
|
||||
"@mantine/hooks": "^7.17.1",
|
||||
"@sinclair/typebox": "0.34.30",
|
||||
"@tanstack/react-form": "^1.0.5",
|
||||
"@uiw/react-codemirror": "^4.23.10",
|
||||
"@xyflow/react": "^12.4.4",
|
||||
@@ -61,29 +62,33 @@
|
||||
"bcryptjs": "^3.0.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"fast-xml-parser": "^5.0.8",
|
||||
"json-schema-form-react": "^0.0.2",
|
||||
"hono": "4.8.3",
|
||||
"json-schema-library": "10.0.0-rc7",
|
||||
"json-schema-to-ts": "^3.1.1",
|
||||
"kysely": "^0.27.6",
|
||||
"hono": "^4.7.11",
|
||||
"jsonv-ts": "0.8.2",
|
||||
"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/workers-types": "^4.20250606.0",
|
||||
"@dagrejs/dagre": "^1.1.4",
|
||||
"@hono/typebox-validator": "^0.3.3",
|
||||
"@hono/vite-dev-server": "^0.19.1",
|
||||
"@hono/vite-dev-server": "^0.21.0",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@libsql/kysely-libsql": "^0.4.1",
|
||||
"@libsql/client": "^0.15.9",
|
||||
"@mantine/modals": "^7.17.1",
|
||||
"@mantine/notifications": "^7.17.1",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@rjsf/core": "5.22.2",
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@tabler/icons-react": "3.18.0",
|
||||
"@tailwindcss/postcss": "^4.0.12",
|
||||
"@tailwindcss/vite": "^4.0.12",
|
||||
@@ -99,11 +104,11 @@
|
||||
"dotenv": "^16.4.7",
|
||||
"jotai": "^2.12.2",
|
||||
"jsdom": "^26.0.0",
|
||||
"jsonv-ts": "^0.1.0",
|
||||
"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",
|
||||
@@ -122,10 +127,12 @@
|
||||
"tsx": "^4.19.3",
|
||||
"uuid": "^11.1.0",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-circular-dependency": "^0.5.0",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.0.9",
|
||||
"wouter": "^3.6.0",
|
||||
"@cloudflare/workers-types": "^4.20250606.0"
|
||||
"wrangler": "^4.37.1",
|
||||
"miniflare": "^4.20250913.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@hono/node-server": "^1.14.3"
|
||||
@@ -158,16 +165,6 @@
|
||||
"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",
|
||||
@@ -178,16 +175,35 @@
|
||||
"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",
|
||||
"require": "./dist/plugins/index.js"
|
||||
},
|
||||
"./adapter/sqlite": {
|
||||
"types": "./dist/types/adapter/sqlite/edge.d.ts",
|
||||
"import": {
|
||||
"workerd": "./dist/adapter/sqlite/edge.js",
|
||||
"edge-light": "./dist/adapter/sqlite/edge.js",
|
||||
"netlify": "./dist/adapter/sqlite/edge.js",
|
||||
"vercel": "./dist/adapter/sqlite/edge.js",
|
||||
"browser": "./dist/adapter/sqlite/edge.js",
|
||||
"bun": "./dist/adapter/sqlite/bun.js",
|
||||
"node": "./dist/adapter/sqlite/node.js",
|
||||
"default": "./dist/adapter/sqlite/node.js"
|
||||
},
|
||||
"require": "./dist/adapter/sqlite/node.js"
|
||||
},
|
||||
"./adapter/cloudflare": {
|
||||
"types": "./dist/types/adapter/cloudflare/index.d.ts",
|
||||
"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"
|
||||
@@ -229,7 +245,23 @@
|
||||
},
|
||||
"./dist/main.css": "./dist/ui/main.css",
|
||||
"./dist/styles.css": "./dist/ui/styles.css",
|
||||
"./dist/manifest.json": "./dist/static/.vite/manifest.json"
|
||||
"./dist/manifest.json": "./dist/static/.vite/manifest.json",
|
||||
"./static/*": "./dist/static/*"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"utils": ["./dist/types/core/utils/index.d.ts"],
|
||||
"cli": ["./dist/types/cli/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"],
|
||||
"adapter/vite": ["./dist/types/adapter/vite/index.d.ts"],
|
||||
"adapter/nextjs": ["./dist/types/adapter/nextjs/index.d.ts"],
|
||||
"adapter/react-router": ["./dist/types/adapter/react-router/index.d.ts"],
|
||||
"adapter/bun": ["./dist/types/adapter/bun/index.d.ts"],
|
||||
"adapter/node": ["./dist/types/adapter/node/index.d.ts"],
|
||||
"adapter/sqlite": ["./dist/types/adapter/sqlite/edge.d.ts"]
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
+3
-3
@@ -1,10 +1,10 @@
|
||||
import type { SafeUser } from "auth";
|
||||
import type { SafeUser } from "bknd";
|
||||
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 "core/utils";
|
||||
import { omitKeys } from "bknd/utils";
|
||||
import type { BaseModuleApiOptions } from "modules";
|
||||
|
||||
export type TApiUser = SafeUser;
|
||||
@@ -43,7 +43,7 @@ export type ApiOptions = {
|
||||
} & (
|
||||
| {
|
||||
token?: string;
|
||||
user?: TApiUser;
|
||||
user?: TApiUser | null;
|
||||
}
|
||||
| {
|
||||
request: Request;
|
||||
|
||||
+167
-66
@@ -1,30 +1,70 @@
|
||||
import type { CreateUserPayload } from "auth/AppAuth";
|
||||
import { $console } from "core";
|
||||
import { $console, McpClient } from "bknd/utils";
|
||||
import { Event } from "core/events";
|
||||
import { Connection, type LibSqlCredentials, LibsqlConnection } from "data";
|
||||
import type { em as prototypeEm } from "data/prototype";
|
||||
import { Connection } from "data/connection/Connection";
|
||||
import type { Hono } from "hono";
|
||||
import {
|
||||
ModuleManager,
|
||||
type InitialModuleConfigs,
|
||||
type ModuleBuildContext,
|
||||
type ModuleConfigs,
|
||||
type ModuleManagerOptions,
|
||||
type Modules,
|
||||
ModuleManager,
|
||||
type ModuleBuildContext,
|
||||
type ModuleManagerOptions,
|
||||
} 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 { ServerEnv } from "modules/Controller";
|
||||
import type { IEmailDriver, ICacheDriver } from "core/drivers";
|
||||
|
||||
// biome-ignore format: must be here
|
||||
import { Api, type ApiOptions } from "Api";
|
||||
import type { ServerEnv } from "modules/Controller";
|
||||
|
||||
export type AppPlugin = (app: App) => Promise<void> | void;
|
||||
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>;
|
||||
};
|
||||
export type AppPlugin = (app: App) => AppPluginConfig;
|
||||
|
||||
abstract class AppEvent<A = {}> extends Event<{ app: App } & A> {}
|
||||
export class AppConfigUpdatedEvent extends AppEvent {
|
||||
export class AppConfigUpdatedEvent extends AppEvent<{
|
||||
module: string;
|
||||
config: ModuleConfigs[keyof ModuleConfigs];
|
||||
}> {
|
||||
static override slug = "app-config-updated";
|
||||
}
|
||||
/**
|
||||
* @type {Event<{ app: App }>}
|
||||
*/
|
||||
export class AppBuiltEvent extends AppEvent {
|
||||
static override slug = "app-built";
|
||||
}
|
||||
@@ -50,55 +90,108 @@ export type AppOptions = {
|
||||
seed?: (ctx: ModuleBuildContext & { app: App }) => Promise<void>;
|
||||
manager?: Omit<ModuleManagerOptions, "initial" | "onUpdated" | "seed">;
|
||||
asyncEventsMode?: "sync" | "async" | "none";
|
||||
drivers?: {
|
||||
email?: IEmailDriver;
|
||||
cache?: ICacheDriver;
|
||||
};
|
||||
mode?: "db" | "code";
|
||||
readonly?: boolean;
|
||||
};
|
||||
export type CreateAppConfig = {
|
||||
connection?:
|
||||
| Connection
|
||||
| {
|
||||
// @deprecated
|
||||
type: "libsql";
|
||||
config: LibSqlCredentials;
|
||||
}
|
||||
| LibSqlCredentials;
|
||||
initialConfig?: InitialModuleConfigs;
|
||||
connection?: Connection | { url: string };
|
||||
config?: PartialRec<ModuleConfigs>;
|
||||
options?: AppOptions;
|
||||
};
|
||||
|
||||
export type AppConfig = InitialModuleConfigs;
|
||||
export type AppConfig = { version: number } & ModuleConfigs;
|
||||
export type LocalApiOptions = Request | ApiOptions;
|
||||
|
||||
export class App {
|
||||
export class App<
|
||||
C extends Connection = Connection,
|
||||
Config extends PartialRec<ModuleConfigs> = PartialRec<ModuleConfigs>,
|
||||
Options extends AppOptions = AppOptions,
|
||||
> {
|
||||
static readonly Events = AppEvents;
|
||||
|
||||
modules: ModuleManager;
|
||||
adminController?: AdminController;
|
||||
_id: string = crypto.randomUUID();
|
||||
plugins: Map<string, AppPluginConfig> = new Map();
|
||||
drivers: Options["drivers"] = {};
|
||||
|
||||
private trigger_first_boot = false;
|
||||
private plugins: AppPlugin[];
|
||||
private _building: boolean = false;
|
||||
private _systemController: SystemController | null = null;
|
||||
|
||||
constructor(
|
||||
private connection: Connection,
|
||||
_initialConfig?: InitialModuleConfigs,
|
||||
private options?: AppOptions,
|
||||
public connection: C,
|
||||
_config?: Config,
|
||||
public options?: Options,
|
||||
) {
|
||||
this.plugins = options?.plugins ?? [];
|
||||
this.modules = new ModuleManager(connection, {
|
||||
this.drivers = options?.drivers ?? {};
|
||||
|
||||
for (const plugin of options?.plugins ?? []) {
|
||||
const config = plugin(this);
|
||||
if (this.plugins.has(config.name)) {
|
||||
throw new Error(`Plugin ${config.name} already registered`);
|
||||
}
|
||||
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, {
|
||||
...(options?.manager ?? {}),
|
||||
initial: _initialConfig,
|
||||
initial: _config,
|
||||
onUpdated: this.onUpdated.bind(this),
|
||||
onFirstBoot: this.onFirstBoot.bind(this),
|
||||
onServerInit: this.onServerInit.bind(this),
|
||||
onModulesBuilt: this.onModulesBuilt.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;
|
||||
}
|
||||
|
||||
async build(options?: { sync?: boolean; fetch?: boolean; forceBuild?: boolean }) {
|
||||
protected async runPlugins<Key extends keyof AppPluginConfig>(
|
||||
key: Key,
|
||||
...args: any[]
|
||||
): Promise<{ name: string; result: any }[]> {
|
||||
const results: { name: string; result: any }[] = [];
|
||||
for (const [name, config] of this.plugins) {
|
||||
try {
|
||||
if (key in config && config[key]) {
|
||||
const fn = config[key];
|
||||
if (fn && typeof fn === "function") {
|
||||
$console.debug(`[Plugin:${name}] ${key}`);
|
||||
// @ts-expect-error
|
||||
const result = await fn(...args);
|
||||
results.push({
|
||||
name,
|
||||
result,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
$console.warn(`[Plugin:${name}] error running "${key}"`, String(e));
|
||||
}
|
||||
}
|
||||
return results as any;
|
||||
}
|
||||
|
||||
async build(options?: { sync?: boolean; forceBuild?: boolean; [key: string]: any }) {
|
||||
// prevent multiple concurrent builds
|
||||
if (this._building) {
|
||||
while (this._building) {
|
||||
@@ -106,24 +199,24 @@ export class App {
|
||||
}
|
||||
if (!options?.forceBuild) return;
|
||||
}
|
||||
|
||||
await this.runPlugins("beforeBuild");
|
||||
this._building = true;
|
||||
|
||||
if (options?.sync) this.modules.ctx().flags.sync_required = true;
|
||||
await this.modules.build({ fetch: options?.fetch });
|
||||
await this.modules.build();
|
||||
|
||||
const { guard, server } = this.modules.ctx();
|
||||
const { guard } = this.modules.ctx();
|
||||
|
||||
// load system controller
|
||||
guard.registerPermissions(Object.values(SystemPermissions));
|
||||
server.route("/api/system", new SystemController(this).getController());
|
||||
|
||||
// load plugins
|
||||
if (this.plugins.length > 0) {
|
||||
await Promise.all(this.plugins.map((plugin) => plugin(this)));
|
||||
}
|
||||
this._systemController = new SystemController(this);
|
||||
this._systemController.register(this);
|
||||
|
||||
// emit built event
|
||||
$console.log("App built");
|
||||
await this.emgr.emit(new AppBuiltEvent({ app: this }));
|
||||
await this.runPlugins("onBuilt");
|
||||
|
||||
// first boot is set from ModuleManager when there wasn't a config table
|
||||
if (this.trigger_first_boot) {
|
||||
@@ -138,10 +231,6 @@ export class App {
|
||||
this._building = false;
|
||||
}
|
||||
|
||||
mutateConfig<Module extends keyof Modules>(module: Module) {
|
||||
return this.modules.mutateConfigSafe(module);
|
||||
}
|
||||
|
||||
get server() {
|
||||
return this.modules.server;
|
||||
}
|
||||
@@ -150,6 +239,10 @@ export class App {
|
||||
return this.modules.ctx().em;
|
||||
}
|
||||
|
||||
get mcp() {
|
||||
return this._systemController?._mcpServer;
|
||||
}
|
||||
|
||||
get fetch(): Hono["fetch"] {
|
||||
return this.server.fetch as any;
|
||||
}
|
||||
@@ -208,6 +301,18 @@ 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".
|
||||
@@ -220,15 +325,16 @@ export class App {
|
||||
$console.log("App config updated", module);
|
||||
// @todo: potentially double syncing
|
||||
await this.build({ sync: true });
|
||||
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this }));
|
||||
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this, module, config }));
|
||||
}
|
||||
|
||||
async onFirstBoot() {
|
||||
protected async onFirstBoot() {
|
||||
$console.log("App first boot");
|
||||
this.trigger_first_boot = true;
|
||||
await this.runPlugins("onFirstBoot");
|
||||
}
|
||||
|
||||
async onServerInit(server: Hono<ServerEnv>) {
|
||||
protected async onServerInit(server: Hono<ServerEnv>) {
|
||||
server.use(async (c, next) => {
|
||||
c.set("app", this);
|
||||
await this.emgr.emit(new AppRequest({ app: this, request: c.req.raw }));
|
||||
@@ -258,35 +364,30 @@ export class App {
|
||||
if (this.options?.manager?.onServerInit) {
|
||||
this.options.manager.onServerInit(server);
|
||||
}
|
||||
|
||||
await this.runPlugins("onServerInit", server);
|
||||
}
|
||||
|
||||
protected async onModulesBuilt(ctx: ModuleBuildContext) {
|
||||
const results = (await this.runPlugins("schema")) as {
|
||||
name: string;
|
||||
result: ReturnType<typeof prototypeEm>;
|
||||
}[];
|
||||
if (results.length > 0) {
|
||||
for (const { name, result } of results) {
|
||||
if (result) {
|
||||
$console.log(`[Plugin:${name}] schema`);
|
||||
ctx.helper.ensureSchema(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createApp(config: CreateAppConfig = {}) {
|
||||
let connection: Connection | undefined = undefined;
|
||||
|
||||
try {
|
||||
if (Connection.isConnection(config.connection)) {
|
||||
connection = config.connection;
|
||||
} else if (typeof config.connection === "object") {
|
||||
if ("type" in config.connection) {
|
||||
$console.warn(
|
||||
"Using deprecated connection type 'libsql', use the 'config' object directly.",
|
||||
);
|
||||
connection = new LibsqlConnection(config.connection.config);
|
||||
} else {
|
||||
connection = new LibsqlConnection(config.connection);
|
||||
}
|
||||
} else {
|
||||
connection = new LibsqlConnection({ url: ":memory:" });
|
||||
$console.warn("No connection provided, using in-memory database");
|
||||
}
|
||||
} catch (e) {
|
||||
$console.error("Could not create connection", e);
|
||||
}
|
||||
|
||||
if (!connection) {
|
||||
if (!config.connection || !Connection.isConnection(config.connection)) {
|
||||
throw new Error("Invalid connection");
|
||||
}
|
||||
|
||||
return new App(connection, config.initialConfig, config.options);
|
||||
return new App(config.connection, config.config, config.options);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { TestRunner } from "core/test";
|
||||
import type { BkndConfig, DefaultArgs, FrameworkOptions, RuntimeOptions } from "./index";
|
||||
import type { BkndConfig, DefaultArgs } from "./index";
|
||||
import type { App } from "App";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
export function adapterTestSuite<
|
||||
Config extends BkndConfig = BkndConfig,
|
||||
@@ -13,24 +14,17 @@ export function adapterTestSuite<
|
||||
label = "app",
|
||||
overrides = {},
|
||||
}: {
|
||||
makeApp: (
|
||||
config: Config,
|
||||
args?: Args,
|
||||
opts?: RuntimeOptions | FrameworkOptions,
|
||||
) => Promise<App>;
|
||||
makeHandler?: (
|
||||
config?: Config,
|
||||
args?: Args,
|
||||
opts?: RuntimeOptions | FrameworkOptions,
|
||||
) => (request: Request) => Promise<Response>;
|
||||
makeApp: (config: Config, args?: Args) => Promise<App>;
|
||||
makeHandler?: (config?: Config, args?: Args) => (request: Request) => Promise<Response>;
|
||||
label?: string;
|
||||
overrides?: {
|
||||
dbUrl?: string;
|
||||
};
|
||||
},
|
||||
) {
|
||||
const { test, expect, mock } = testRunner;
|
||||
const id = crypto.randomUUID();
|
||||
const { test, expect, mock, beforeAll, afterAll } = testRunner;
|
||||
beforeAll(() => disableConsoleLog());
|
||||
afterAll(() => enableConsoleLog());
|
||||
|
||||
test(`creates ${label}`, async () => {
|
||||
const beforeBuild = mock(async () => null) as any;
|
||||
@@ -39,7 +33,7 @@ export function adapterTestSuite<
|
||||
const config = {
|
||||
app: (env) => ({
|
||||
connection: { url: env.url },
|
||||
initialConfig: {
|
||||
config: {
|
||||
server: { cors: { origin: env.origin } },
|
||||
},
|
||||
}),
|
||||
@@ -53,11 +47,10 @@ 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(1);
|
||||
expect(beforeBuild).toHaveBeenCalledTimes(2);
|
||||
expect(onBuilt).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -68,7 +61,7 @@ export function adapterTestSuite<
|
||||
return { res, data };
|
||||
};
|
||||
|
||||
test("responds with the same app id", async () => {
|
||||
/* test.skip("responds with the same app id", async () => {
|
||||
const fetcher = makeHandler(undefined, undefined, { id });
|
||||
|
||||
const { res, data } = await getConfig(fetcher);
|
||||
@@ -77,14 +70,14 @@ export function adapterTestSuite<
|
||||
expect(data.server.cors.origin).toEqual("localhost");
|
||||
});
|
||||
|
||||
test("creates fresh & responds to api config", async () => {
|
||||
test.skip("creates fresh & responds to api config", async () => {
|
||||
// set the same id, but force recreate
|
||||
const fetcher = makeHandler(undefined, undefined, { id, force: true });
|
||||
const fetcher = makeHandler(undefined, undefined, { id });
|
||||
|
||||
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, o) => (request: Request) => astro.serve(c, a, o)({ request }),
|
||||
makeHandler: (c, a) => (request: Request) => astro.serve(c, a)({ request }),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type FrameworkBkndConfig, createFrameworkApp, type FrameworkOptions } from "bknd/adapter";
|
||||
import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter";
|
||||
|
||||
type AstroEnv = NodeJS.ProcessEnv;
|
||||
type TAstro = {
|
||||
@@ -9,17 +9,12 @@ 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, opts);
|
||||
return await createFrameworkApp(config, args ?? import.meta.env);
|
||||
}
|
||||
|
||||
export function serve<Env = AstroEnv>(
|
||||
config: AstroBkndConfig<Env> = {},
|
||||
args: Env = {} as Env,
|
||||
opts?: FrameworkOptions,
|
||||
) {
|
||||
export function serve<Env = AstroEnv>(config: AstroBkndConfig<Env> = {}, args: Env = {} as Env) {
|
||||
return async (fnArgs: TAstro) => {
|
||||
return (await getApp(config, args, opts)).fetch(fnArgs.request);
|
||||
return (await getApp(config, args)).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, type RuntimeOptions } from "bknd/adapter";
|
||||
import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter";
|
||||
|
||||
type AwsLambdaEnv = object;
|
||||
export type AwsLambdaBkndConfig<Env extends AwsLambdaEnv = AwsLambdaEnv> =
|
||||
@@ -20,7 +20,6 @@ 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,
|
||||
@@ -57,17 +56,15 @@ 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, opts);
|
||||
const app = await createApp(config, args);
|
||||
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, o) => async (request: Request) => {
|
||||
const app = await awsLambda.createApp(c, a, o);
|
||||
makeHandler: (c, a) => async (request: Request) => {
|
||||
const app = await awsLambda.createApp(c, a);
|
||||
return app.fetch(request);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import path from "node:path";
|
||||
import { type RuntimeBkndConfig, createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
|
||||
import { registerLocalMediaAdapter } from "bknd/adapter/node";
|
||||
import { config } from "bknd/core";
|
||||
import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter";
|
||||
import { registerLocalMediaAdapter } from ".";
|
||||
import { config, type App } from "bknd";
|
||||
import type { ServeOptions } from "bun";
|
||||
import { serveStatic } from "hono/bun";
|
||||
|
||||
@@ -11,30 +11,34 @@ type BunEnv = Bun.Env;
|
||||
export type BunBkndConfig<Env = BunEnv> = RuntimeBkndConfig<Env> & Omit<ServeOptions, "fetch">;
|
||||
|
||||
export async function createApp<Env = BunEnv>(
|
||||
{ distPath, ...config }: BunBkndConfig<Env> = {},
|
||||
{ distPath, serveStatic: _serveStatic, ...config }: BunBkndConfig<Env> = {},
|
||||
args: Env = {} as Env,
|
||||
opts?: RuntimeOptions,
|
||||
) {
|
||||
const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static");
|
||||
registerLocalMediaAdapter();
|
||||
|
||||
return await createRuntimeApp(
|
||||
{
|
||||
serveStatic:
|
||||
_serveStatic ??
|
||||
serveStatic({
|
||||
root,
|
||||
}),
|
||||
...config,
|
||||
serveStatic: serveStatic({ root }),
|
||||
},
|
||||
args ?? (process.env as Env),
|
||||
opts,
|
||||
);
|
||||
}
|
||||
|
||||
export function createHandler<Env = BunEnv>(
|
||||
config: BunBkndConfig<Env> = {},
|
||||
args: Env = {} as Env,
|
||||
opts?: RuntimeOptions,
|
||||
) {
|
||||
let app: App | undefined;
|
||||
return async (req: Request) => {
|
||||
const app = await createApp(config, args ?? (process.env as Env), opts);
|
||||
if (!app) {
|
||||
app = await createApp(config, args ?? (process.env as Env));
|
||||
}
|
||||
return app.fetch(req);
|
||||
};
|
||||
}
|
||||
@@ -43,16 +47,16 @@ export function serve<Env = BunEnv>(
|
||||
{
|
||||
distPath,
|
||||
connection,
|
||||
initialConfig,
|
||||
config: _config,
|
||||
options,
|
||||
port = config.server.default_port,
|
||||
onBuilt,
|
||||
buildConfig,
|
||||
adminOptions,
|
||||
serveStatic,
|
||||
...serveOptions
|
||||
}: BunBkndConfig<Env> = {},
|
||||
args: Env = {} as Env,
|
||||
opts?: RuntimeOptions,
|
||||
) {
|
||||
Bun.serve({
|
||||
...serveOptions,
|
||||
@@ -60,17 +64,17 @@ export function serve<Env = BunEnv>(
|
||||
fetch: createHandler(
|
||||
{
|
||||
connection,
|
||||
initialConfig,
|
||||
config: _config,
|
||||
options,
|
||||
onBuilt,
|
||||
buildConfig,
|
||||
adminOptions,
|
||||
distPath,
|
||||
serveStatic,
|
||||
},
|
||||
args,
|
||||
opts,
|
||||
),
|
||||
});
|
||||
|
||||
console.log(`Server is running on http://localhost:${port}`);
|
||||
console.info(`Server is running on http://localhost:${port}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
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 { Database } from "bun:sqlite";
|
||||
import { GenericSqliteConnection } from "data/connection/sqlite/GenericSqliteConnection";
|
||||
|
||||
describe("BunSqliteConnection", () => {
|
||||
connectionTestSuite(bunTestRunner, {
|
||||
makeConnection: () => ({
|
||||
connection: bunSqlite({ database: new Database(":memory:") }),
|
||||
dispose: async () => {},
|
||||
}),
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Database } from "bun:sqlite";
|
||||
import {
|
||||
genericSqlite,
|
||||
type GenericSqliteConnection,
|
||||
type GenericSqliteConnectionConfig,
|
||||
} from "bknd";
|
||||
import { omitKeys } from "bknd/utils";
|
||||
|
||||
export type BunSqliteConnection = GenericSqliteConnection<Database>;
|
||||
export type BunSqliteConnectionConfig = Omit<
|
||||
GenericSqliteConnectionConfig<Database>,
|
||||
"name" | "supports"
|
||||
> &
|
||||
({ database?: Database; url?: never } | { url?: string; database?: never });
|
||||
|
||||
export function bunSqlite(config?: BunSqliteConnectionConfig) {
|
||||
let db: Database | undefined;
|
||||
if (config) {
|
||||
if ("database" in config && config.database) {
|
||||
db = config.database;
|
||||
} else if (config.url) {
|
||||
db = new Database(config.url);
|
||||
}
|
||||
}
|
||||
|
||||
if (!db) {
|
||||
db = new Database(":memory:");
|
||||
}
|
||||
|
||||
return genericSqlite(
|
||||
"bun-sqlite",
|
||||
db,
|
||||
(utils) => {
|
||||
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"]),
|
||||
);
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
export * from "./bun.adapter";
|
||||
export * from "../node/storage";
|
||||
export * from "./connection/BunSqliteConnection";
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { expect, test, mock } from "bun:test";
|
||||
import { expect, test, mock, describe, beforeEach, afterEach, afterAll, beforeAll } from "bun:test";
|
||||
|
||||
export const bunTestRunner = {
|
||||
describe,
|
||||
expect,
|
||||
test,
|
||||
mock,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
afterAll,
|
||||
beforeAll,
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { inspect } from "node:util";
|
||||
|
||||
export type BindingTypeMap = {
|
||||
D1Database: D1Database;
|
||||
KVNamespace: KVNamespace;
|
||||
@@ -12,7 +14,11 @@ export function getBindings<T extends GetBindingType>(env: any, type: T): Bindin
|
||||
const bindings: BindingMap<T>[] = [];
|
||||
for (const key in env) {
|
||||
try {
|
||||
if (env[key] && (env[key] as any).constructor.name === type) {
|
||||
if (
|
||||
(env[key] as any).constructor.name === type ||
|
||||
String(env[key]) === `[object ${type}]` ||
|
||||
inspect(env[key]).includes(type)
|
||||
) {
|
||||
bindings.push({
|
||||
key,
|
||||
value: env[key] as BindingTypeMap[T],
|
||||
|
||||
@@ -1,57 +1,58 @@
|
||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||
import { makeApp } from "./modes/fresh";
|
||||
import { makeConfig, type CfMakeConfigArgs } from "./config";
|
||||
import { makeConfig, type CloudflareContext } from "./config";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import type { CloudflareBkndConfig } from "./cloudflare-workers.adapter";
|
||||
import { type CloudflareBkndConfig, createApp } from "./cloudflare-workers.adapter";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
/* beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog); */
|
||||
|
||||
describe("cf adapter", () => {
|
||||
const DB_URL = ":memory:";
|
||||
const $ctx = (env?: any, request?: Request, ctx?: ExecutionContext) => ({
|
||||
request: request ?? (null as any),
|
||||
env: env ?? { DB_URL },
|
||||
env: env ?? { url: DB_URL },
|
||||
ctx: ctx ?? (null as any),
|
||||
});
|
||||
|
||||
it("makes config", async () => {
|
||||
expect(
|
||||
makeConfig(
|
||||
{
|
||||
connection: { url: DB_URL },
|
||||
},
|
||||
$ctx({ DB_URL }),
|
||||
),
|
||||
).toEqual({ connection: { url: DB_URL } });
|
||||
const staticConfig = await makeConfig(
|
||||
{
|
||||
connection: { url: DB_URL },
|
||||
config: { data: { basepath: DB_URL } },
|
||||
},
|
||||
$ctx({ DB_URL }),
|
||||
);
|
||||
expect(staticConfig.config).toEqual({ data: { basepath: DB_URL } });
|
||||
expect(staticConfig.connection).toBeDefined();
|
||||
|
||||
expect(
|
||||
makeConfig(
|
||||
{
|
||||
app: (env) => ({
|
||||
connection: { url: env.DB_URL },
|
||||
}),
|
||||
},
|
||||
$ctx({ DB_URL }),
|
||||
),
|
||||
).toEqual({ connection: { url: DB_URL } });
|
||||
const dynamicConfig = await makeConfig(
|
||||
{
|
||||
app: (env) => ({
|
||||
config: { data: { basepath: env.DB_URL } },
|
||||
connection: { url: env.DB_URL },
|
||||
}),
|
||||
},
|
||||
$ctx({ DB_URL }),
|
||||
);
|
||||
expect(dynamicConfig.config).toEqual({ data: { basepath: DB_URL } });
|
||||
expect(dynamicConfig.connection).toBeDefined();
|
||||
});
|
||||
|
||||
adapterTestSuite<CloudflareBkndConfig, CfMakeConfigArgs<any>>(bunTestRunner, {
|
||||
makeApp: async (c, a, o) => {
|
||||
return await makeApp(c, { env: a } as any, o);
|
||||
adapterTestSuite<CloudflareBkndConfig, CloudflareContext<any>>(bunTestRunner, {
|
||||
makeApp: async (c, a) => {
|
||||
return await createApp(c, { env: a } as any);
|
||||
},
|
||||
makeHandler: (c, a, o) => {
|
||||
makeHandler: (c, a) => {
|
||||
console.log("args", a);
|
||||
return async (request: any) => {
|
||||
const app = await makeApp(
|
||||
const app = await createApp(
|
||||
// needs a fallback, otherwise tries to launch D1
|
||||
c ?? {
|
||||
connection: { url: DB_URL },
|
||||
},
|
||||
a!,
|
||||
o,
|
||||
a as any,
|
||||
);
|
||||
return app.fetch(request);
|
||||
};
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
import type { RuntimeBkndConfig } from "bknd/adapter";
|
||||
import { Hono } from "hono";
|
||||
import { serveStatic } from "hono/cloudflare-workers";
|
||||
import { getFresh } from "./modes/fresh";
|
||||
import { getCached } from "./modes/cached";
|
||||
import { getDurable } from "./modes/durable";
|
||||
import type { App } from "bknd";
|
||||
import { $console } from "core";
|
||||
import type { MaybePromise } from "bknd";
|
||||
import { $console } from "bknd/utils";
|
||||
import { createRuntimeApp } from "bknd/adapter";
|
||||
import { registerAsyncsExecutionContext, makeConfig, type CloudflareContext } from "./config";
|
||||
|
||||
declare global {
|
||||
namespace Cloudflare {
|
||||
@@ -17,12 +16,10 @@ declare global {
|
||||
|
||||
export type CloudflareEnv = Cloudflare.Env;
|
||||
export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> & {
|
||||
mode?: "warm" | "fresh" | "cache" | "durable";
|
||||
bindings?: (args: Env) => {
|
||||
bindings?: (args: Env) => MaybePromise<{
|
||||
kv?: KVNamespace;
|
||||
dobj?: DurableObjectNamespace;
|
||||
db?: D1Database;
|
||||
};
|
||||
}>;
|
||||
d1?: {
|
||||
session?: boolean;
|
||||
transport?: "header" | "cookie";
|
||||
@@ -33,13 +30,30 @@ export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> &
|
||||
keepAliveSeconds?: number;
|
||||
forceHttps?: boolean;
|
||||
manifest?: string;
|
||||
registerMedia?: boolean | ((env: Env) => void);
|
||||
};
|
||||
|
||||
export type Context<Env = CloudflareEnv> = {
|
||||
request: Request;
|
||||
env: Env;
|
||||
ctx: ExecutionContext;
|
||||
};
|
||||
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 function serve<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
config: CloudflareBkndConfig<Env> = {},
|
||||
@@ -78,25 +92,8 @@ export function serve<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
}
|
||||
}
|
||||
|
||||
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}`);
|
||||
}
|
||||
const context = { request, env, ctx } as CloudflareContext<Env>;
|
||||
const app = await createApp(config, context);
|
||||
|
||||
return app.fetch(request, env, ctx);
|
||||
},
|
||||
|
||||
@@ -1,13 +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 { D1Connection } from "./connection/D1Connection";
|
||||
import { d1Sqlite } from "./connection/D1Connection";
|
||||
import type { CloudflareBkndConfig, CloudflareEnv } from ".";
|
||||
import { App } from "bknd";
|
||||
import { makeConfig as makeAdapterConfig } from "bknd/adapter";
|
||||
import type { Context, ExecutionContext } from "hono";
|
||||
import { $console } from "core";
|
||||
import type { Context as HonoContext, ExecutionContext } from "hono";
|
||||
import { $console } from "bknd/utils";
|
||||
import { setCookie } from "hono/cookie";
|
||||
|
||||
export const constants = {
|
||||
@@ -20,10 +22,10 @@ export const constants = {
|
||||
},
|
||||
};
|
||||
|
||||
export type CfMakeConfigArgs<Env extends CloudflareEnv = CloudflareEnv> = {
|
||||
export type CloudflareContext<Env extends CloudflareEnv = CloudflareEnv> = {
|
||||
env: Env;
|
||||
ctx?: ExecutionContext;
|
||||
request?: Request;
|
||||
ctx: ExecutionContext;
|
||||
request: Request;
|
||||
};
|
||||
|
||||
function getCookieValue(cookies: string | null, name: string) {
|
||||
@@ -65,7 +67,7 @@ export function d1SessionHelper(config: CloudflareBkndConfig<any>) {
|
||||
|
||||
return undefined;
|
||||
},
|
||||
set: (c: Context, d1?: D1DatabaseSession) => {
|
||||
set: (c: HonoContext, d1?: D1DatabaseSession) => {
|
||||
if (!d1 || !config.d1?.session) return;
|
||||
|
||||
const session = d1.getBookmark();
|
||||
@@ -87,65 +89,88 @@ export function d1SessionHelper(config: CloudflareBkndConfig<any>) {
|
||||
}
|
||||
|
||||
let media_registered: boolean = false;
|
||||
export function makeConfig<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
export async function makeConfig<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
config: CloudflareBkndConfig<Env>,
|
||||
args?: CfMakeConfigArgs<Env>,
|
||||
args?: Partial<CloudflareContext<Env>>,
|
||||
) {
|
||||
if (!media_registered) {
|
||||
registerMedia(args?.env as any);
|
||||
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 = makeAdapterConfig(config, args?.env);
|
||||
const appConfig = await makeAdapterConfig(config, args?.env);
|
||||
|
||||
if (args?.env) {
|
||||
const bindings = config.bindings?.(args?.env);
|
||||
// if connection instance is given, don't do anything
|
||||
// other than checking if D1 session is defined
|
||||
if (Connection.isConnection(appConfig.connection)) {
|
||||
if (config.d1?.session) {
|
||||
// we cannot guarantee that db was opened with session
|
||||
throw new Error(
|
||||
"D1 session don't work when D1 is directly given as connection. Define it in bindings instead.",
|
||||
);
|
||||
}
|
||||
// if connection is given, try to open with unified sqlite adapter
|
||||
} else if (appConfig.connection) {
|
||||
appConfig.connection = sqlite(appConfig.connection) as any;
|
||||
|
||||
// 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 sessionHelper = d1SessionHelper(config);
|
||||
const sessionId = sessionHelper.get(args.request);
|
||||
let session: D1DatabaseSession | undefined;
|
||||
let db: D1Database | undefined;
|
||||
|
||||
if (!appConfig.connection) {
|
||||
let db: D1Database | undefined;
|
||||
if (bindings?.db) {
|
||||
$console.log("Using database from bindings");
|
||||
db = bindings.db;
|
||||
} else if (Object.keys(args).length > 0) {
|
||||
const binding = getBinding(args.env, "D1Database");
|
||||
if (binding) {
|
||||
$console.log(`Using database from env "${binding.key}"`);
|
||||
db = binding.value;
|
||||
}
|
||||
}
|
||||
// if db is given in bindings, use it
|
||||
if (bindings?.db) {
|
||||
$console.debug("Using database from bindings");
|
||||
db = bindings.db;
|
||||
|
||||
if (db) {
|
||||
if (config.d1?.session) {
|
||||
session = db.withSession(sessionId ?? config.d1?.first);
|
||||
appConfig.connection = new D1Connection({ binding: session });
|
||||
} else {
|
||||
appConfig.connection = new D1Connection({ binding: db });
|
||||
}
|
||||
} else {
|
||||
throw new Error("No database connection given");
|
||||
// scan for D1Database in args
|
||||
} else {
|
||||
const binding = getBinding(args.env, "D1Database");
|
||||
if (binding) {
|
||||
$console.debug(`Using database from env "${binding.key}"`);
|
||||
db = binding.value;
|
||||
}
|
||||
}
|
||||
|
||||
if (config.d1?.session) {
|
||||
appConfig.options = {
|
||||
...appConfig.options,
|
||||
manager: {
|
||||
...appConfig.options?.manager,
|
||||
onServerInit: (server) => {
|
||||
server.use(async (c, next) => {
|
||||
sessionHelper.set(c, session);
|
||||
await next();
|
||||
});
|
||||
// if db is found, check if session is requested
|
||||
if (db) {
|
||||
if (config.d1?.session) {
|
||||
session = db.withSession(sessionId ?? config.d1?.first);
|
||||
if (!session) {
|
||||
throw new Error("Couldn't create session");
|
||||
}
|
||||
|
||||
appConfig.connection = d1Sqlite({ binding: session });
|
||||
appConfig.options = {
|
||||
...appConfig.options,
|
||||
manager: {
|
||||
...appConfig.options?.manager,
|
||||
onServerInit: (server) => {
|
||||
server.use(async (c, next) => {
|
||||
sessionHelper.set(c, session);
|
||||
await next();
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
} else {
|
||||
appConfig.connection = d1Sqlite({ binding: db });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Connection.isConnection(appConfig.connection)) {
|
||||
throw new Error("Couldn't find database connection");
|
||||
}
|
||||
|
||||
return appConfig;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,65 +1,75 @@
|
||||
/// <reference types="@cloudflare/workers-types" />
|
||||
|
||||
import { KyselyPluginRunner, SqliteConnection, SqliteIntrospector } from "bknd/data";
|
||||
import type { QB } from "data/connection/Connection";
|
||||
import { type DatabaseIntrospector, Kysely, ParseJSONResultsPlugin } from "kysely";
|
||||
import { D1Dialect } from "kysely-d1";
|
||||
import { genericSqlite, type GenericSqliteConnection } from "bknd";
|
||||
import type { QueryResult } from "kysely";
|
||||
|
||||
export type D1SqliteConnection = GenericSqliteConnection<D1Database>;
|
||||
|
||||
export type D1ConnectionConfig<DB extends D1Database | D1DatabaseSession = D1Database> = {
|
||||
binding: DB;
|
||||
};
|
||||
|
||||
class CustomD1Dialect extends D1Dialect {
|
||||
override createIntrospector(db: Kysely<any>): DatabaseIntrospector {
|
||||
return new SqliteIntrospector(db, {
|
||||
export function d1Sqlite<DB extends D1Database | D1DatabaseSession = D1Database>(
|
||||
config: D1ConnectionConfig<DB>,
|
||||
) {
|
||||
const db = config.binding;
|
||||
|
||||
return genericSqlite(
|
||||
"d1-sqlite",
|
||||
db,
|
||||
(utils) => {
|
||||
const getStmt = (sql: string, parameters?: any[] | readonly any[]) =>
|
||||
db.prepare(sql).bind(...(parameters || []));
|
||||
|
||||
const mapResult = (res: D1Result<any>): QueryResult<any> => {
|
||||
if (res.error) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const numAffectedRows =
|
||||
res.meta.changes > 0 ? utils.parseBigInt(res.meta.changes) : undefined;
|
||||
const insertId = res.meta.last_row_id
|
||||
? utils.parseBigInt(res.meta.last_row_id)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
insertId,
|
||||
numAffectedRows,
|
||||
rows: res.results,
|
||||
// @ts-ignore
|
||||
meta: res.meta,
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
db,
|
||||
batch: async (stmts) => {
|
||||
const res = await db.batch(
|
||||
stmts.map(({ sql, parameters }) => {
|
||||
return getStmt(sql, parameters);
|
||||
}),
|
||||
);
|
||||
return res.map(mapResult);
|
||||
},
|
||||
query: utils.buildQueryFn({
|
||||
all: async (sql, parameters) => {
|
||||
const prep = getStmt(sql, parameters);
|
||||
return mapResult(await prep.all()).rows;
|
||||
},
|
||||
run: async (sql, parameters) => {
|
||||
const prep = getStmt(sql, parameters);
|
||||
return mapResult(await prep.run());
|
||||
},
|
||||
}),
|
||||
close: () => {},
|
||||
};
|
||||
},
|
||||
{
|
||||
supports: {
|
||||
batching: true,
|
||||
softscans: false,
|
||||
},
|
||||
excludeTables: ["_cf_KV", "_cf_METADATA"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class D1Connection<
|
||||
DB extends D1Database | D1DatabaseSession = D1Database,
|
||||
> extends SqliteConnection {
|
||||
protected override readonly supported = {
|
||||
batching: true,
|
||||
};
|
||||
|
||||
constructor(private config: D1ConnectionConfig<DB>) {
|
||||
const plugins = [new ParseJSONResultsPlugin()];
|
||||
|
||||
const kysely = new Kysely({
|
||||
dialect: new CustomD1Dialect({ database: config.binding as D1Database }),
|
||||
plugins,
|
||||
});
|
||||
super(kysely, {}, plugins);
|
||||
}
|
||||
|
||||
get client(): DB {
|
||||
return this.config.binding;
|
||||
}
|
||||
|
||||
protected override async batch<Queries extends QB[]>(
|
||||
queries: [...Queries],
|
||||
): Promise<{
|
||||
[K in keyof Queries]: Awaited<ReturnType<Queries[K]["execute"]>>;
|
||||
}> {
|
||||
const db = this.config.binding;
|
||||
|
||||
const res = await db.batch(
|
||||
queries.map((q) => {
|
||||
const { sql, parameters } = q.compile();
|
||||
return db.prepare(sql).bind(...parameters);
|
||||
}),
|
||||
);
|
||||
|
||||
// let it run through plugins
|
||||
const kyselyPlugins = new KyselyPluginRunner(this.plugins);
|
||||
const data: any = [];
|
||||
for (const r of res) {
|
||||
const rows = await kyselyPlugins.transformResultRows(r.results);
|
||||
data.push(rows);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
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 { d1Sqlite } from "./D1Connection";
|
||||
|
||||
describe("d1Sqlite", async () => {
|
||||
connectionTestSuite(viTestRunner, {
|
||||
makeConnection: async () => {
|
||||
const mf = new Miniflare({
|
||||
modules: true,
|
||||
script: "export default { async fetch() { return new Response(null); } }",
|
||||
d1Databases: ["DB"],
|
||||
});
|
||||
|
||||
const binding = (await mf.getD1Database("DB")) as D1Database;
|
||||
return {
|
||||
connection: d1Sqlite({ binding }),
|
||||
dispose: () => mf.dispose(),
|
||||
};
|
||||
},
|
||||
rawDialectDetails: [
|
||||
"meta.served_by",
|
||||
"meta.duration",
|
||||
"meta.changes",
|
||||
"meta.changed_db",
|
||||
"meta.size_after",
|
||||
"meta.rows_read",
|
||||
"meta.rows_written",
|
||||
],
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
/// <reference types="@cloudflare/workers-types" />
|
||||
|
||||
import { genericSqlite, type GenericSqliteConnection } from "bknd";
|
||||
import type { QueryResult } from "kysely";
|
||||
|
||||
export type DoSqliteConnection = GenericSqliteConnection<DurableObjectState["storage"]["sql"]>;
|
||||
export type DurableObjecSql = DurableObjectState["storage"]["sql"];
|
||||
|
||||
export type DoConnectionConfig<DB extends DurableObjecSql> =
|
||||
| DurableObjectState
|
||||
| {
|
||||
sql: DB;
|
||||
};
|
||||
|
||||
export function doSqlite<DB extends DurableObjecSql>(config: DoConnectionConfig<DB>) {
|
||||
const db = "sql" in config ? config.sql : config.storage.sql;
|
||||
|
||||
return genericSqlite(
|
||||
"do-sqlite",
|
||||
db,
|
||||
(utils) => {
|
||||
// must be async to work with the miniflare mock
|
||||
const getStmt = async (sql: string, parameters?: any[] | readonly any[]) =>
|
||||
db.exec(sql, ...(parameters || []));
|
||||
|
||||
const mapResult = (
|
||||
cursor: SqlStorageCursor<Record<string, SqlStorageValue>>,
|
||||
): QueryResult<any> => {
|
||||
const numAffectedRows =
|
||||
cursor.rowsWritten > 0 ? utils.parseBigInt(cursor.rowsWritten) : undefined;
|
||||
const insertId = undefined;
|
||||
|
||||
const obj = {
|
||||
insertId,
|
||||
numAffectedRows,
|
||||
rows: cursor.toArray() || [],
|
||||
// @ts-ignore
|
||||
meta: {
|
||||
rowsWritten: cursor.rowsWritten,
|
||||
rowsRead: cursor.rowsRead,
|
||||
databaseSize: db.databaseSize,
|
||||
},
|
||||
};
|
||||
//console.info("mapResult", obj);
|
||||
return obj;
|
||||
};
|
||||
|
||||
return {
|
||||
db,
|
||||
batch: async (stmts) => {
|
||||
// @todo: maybe wrap in a transaction?
|
||||
// because d1 implicitly does a transaction on batch
|
||||
return Promise.all(
|
||||
stmts.map(async (stmt) => {
|
||||
return mapResult(await getStmt(stmt.sql, stmt.parameters));
|
||||
}),
|
||||
);
|
||||
},
|
||||
query: utils.buildQueryFn({
|
||||
all: async (sql, parameters) => {
|
||||
const prep = getStmt(sql, parameters);
|
||||
return mapResult(await prep).rows;
|
||||
},
|
||||
run: async (sql, parameters) => {
|
||||
const prep = getStmt(sql, parameters);
|
||||
return mapResult(await prep);
|
||||
},
|
||||
}),
|
||||
close: () => {},
|
||||
};
|
||||
},
|
||||
{
|
||||
supports: {
|
||||
batching: true,
|
||||
softscans: false,
|
||||
},
|
||||
excludeTables: ["_cf_KV", "_cf_METADATA"],
|
||||
},
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user