mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 16:16:02 +00:00
Compare commits
125 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4229477fb6 | |||
| c7e709c4d0 | |||
| de90b3602b | |||
| 7dd486e2db | |||
| 1559d30f25 | |||
| da9a4df404 | |||
| 5b879ac6bf | |||
| c04ad89a86 | |||
| 8abce9f419 | |||
| 93019827b0 | |||
| 1fdfcf02b8 | |||
| 9436b8bac5 | |||
| 8f471a29b0 | |||
| dcf88cf587 | |||
| f9184a71db | |||
| bc1ab0f6d3 | |||
| 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,10 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"bknd": {
|
||||||
|
"url": "http://localhost:3000/mcp",
|
||||||
|
"headers": {
|
||||||
|
"API_KEY": "value"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,10 +12,15 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22.x"
|
||||||
|
|
||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: oven-sh/setup-bun@v1
|
uses: oven-sh/setup-bun@v1
|
||||||
with:
|
with:
|
||||||
bun-version: "1.2.14"
|
bun-version: "1.2.19"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
working-directory: ./app
|
working-directory: ./app
|
||||||
|
|||||||
+3
-1
@@ -31,4 +31,6 @@ packages/media/.env
|
|||||||
.git_old
|
.git_old
|
||||||
docker/tmp
|
docker/tmp
|
||||||
.debug
|
.debug
|
||||||
.history
|
.history
|
||||||
|
.aider*
|
||||||
|
.vercel
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[](https://npmjs.org/package/bknd)
|
[](https://npmjs.org/package/bknd)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
<p align="center" width="100%">
|
<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-examples?hideExplorer=1&embed=1&view=preview&startScript=example-admin-rich&initialPath=%2Fdata%2Fschema" target="_blank">
|
||||||
@@ -9,10 +9,18 @@
|
|||||||
</p>
|
</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!
|
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.**
|
**For documentation and examples, please visit https://docs.bknd.io.**
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
> This project requires Node.js 22 or higher (because of `node:sqlite`).
|
||||||
|
>
|
||||||
> Please keep in mind that **bknd** is still under active development
|
> Please keep in mind that **bknd** is still under active development
|
||||||
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
|
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
|
||||||
|
|
||||||
|
|||||||
+130
-12
@@ -1,6 +1,9 @@
|
|||||||
import { afterAll, afterEach, describe, expect, test } from "bun:test";
|
import { afterEach, describe, test, expect } from "bun:test";
|
||||||
import { App } from "../src";
|
import { App, createApp } from "core/test/utils";
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
|
import { Hono } from "hono";
|
||||||
|
import * as proto from "../src/data/prototype";
|
||||||
|
import { pick } from "lodash-es";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
afterEach(afterAllCleanup);
|
afterEach(afterAllCleanup);
|
||||||
@@ -10,18 +13,133 @@ describe("App tests", async () => {
|
|||||||
const app = new App(dummyConnection);
|
const app = new App(dummyConnection);
|
||||||
await app.build();
|
await app.build();
|
||||||
|
|
||||||
//expect(await app.data?.em.ping()).toBeTrue();
|
expect(await app.em.ping()).toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
/*test.only("what", async () => {
|
test("plugins", async () => {
|
||||||
const app = new App(dummyConnection, {
|
const called: string[] = [];
|
||||||
auth: {
|
const app = createApp({
|
||||||
enabled: true,
|
initialConfig: {
|
||||||
|
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();
|
await app.build();
|
||||||
console.log(app.em.entities.map((e) => e.name));
|
|
||||||
console.log(await app.em.schema().getDiff());
|
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
|
||||||
@@ -3,21 +3,36 @@ import * as adapter from "adapter";
|
|||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||||
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||||
import { bunTestRunner } from "adapter/bun/test";
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
|
import { omitKeys } from "core/utils";
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
beforeAll(disableConsoleLog);
|
||||||
afterAll(enableConsoleLog);
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
describe("adapter", () => {
|
describe("adapter", () => {
|
||||||
it("makes config", () => {
|
it("makes config", async () => {
|
||||||
expect(adapter.makeConfig({})).toEqual({});
|
expect(omitKeys(await adapter.makeConfig({}), ["connection"])).toEqual({});
|
||||||
expect(adapter.makeConfig({}, { env: { TEST: "test" } })).toEqual({});
|
expect(
|
||||||
|
omitKeys(await adapter.makeConfig({}, { env: { TEST: "test" } }), ["connection"]),
|
||||||
|
).toEqual({});
|
||||||
|
|
||||||
// merges everything returned from `app` with the config
|
// merges everything returned from `app` with the config
|
||||||
expect(adapter.makeConfig({ app: (a) => a as any }, { env: { TEST: "test" } })).toEqual({
|
expect(
|
||||||
env: { TEST: "test" },
|
omitKeys(
|
||||||
} as any);
|
await adapter.makeConfig(
|
||||||
|
{ app: (a) => ({ initialConfig: { server: { cors: { origin: a.env.TEST } } } }) },
|
||||||
|
{ env: { TEST: "test" } },
|
||||||
|
),
|
||||||
|
["connection"],
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
initialConfig: { server: { cors: { origin: "test" } } },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* it.only("...", async () => {
|
||||||
|
const app = await adapter.createAdapterApp();
|
||||||
|
}); */
|
||||||
|
|
||||||
it("reuses apps correctly", async () => {
|
it("reuses apps correctly", async () => {
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||||
import { Guard } from "../../src/auth";
|
import { Guard } from "../../src/auth/authorize/Guard";
|
||||||
import { parse } from "../../src/core/utils";
|
|
||||||
import { DataApi } from "../../src/data/api/DataApi";
|
import { DataApi } from "../../src/data/api/DataApi";
|
||||||
import { DataController } from "../../src/data/api/DataController";
|
import { DataController } from "../../src/data/api/DataController";
|
||||||
import { dataConfigSchema } from "../../src/data/data-schema";
|
import { dataConfigSchema } from "../../src/data/data-schema";
|
||||||
import * as proto from "../../src/data/prototype";
|
import * as proto from "../../src/data/prototype";
|
||||||
import { schemaToEm } from "../helper";
|
import { schemaToEm } from "../helper";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
import { parse } from "core/utils/schema";
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
beforeAll(disableConsoleLog);
|
||||||
afterAll(enableConsoleLog);
|
afterAll(enableConsoleLog);
|
||||||
@@ -153,7 +153,7 @@ describe("DataApi", () => {
|
|||||||
const oneBy = api.readOneBy("posts", { where: { title: "baz" }, select: ["title"] });
|
const oneBy = api.readOneBy("posts", { where: { title: "baz" }, select: ["title"] });
|
||||||
const oneByRes = await oneBy;
|
const oneByRes = await oneBy;
|
||||||
expect(oneByRes.data).toEqual({ title: "baz" } as any);
|
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 () => {
|
it("exists/count", async () => {
|
||||||
@@ -202,7 +202,7 @@ describe("DataApi", () => {
|
|||||||
{
|
{
|
||||||
// create many
|
// create many
|
||||||
const res = await api.createMany("posts", payload);
|
const res = await api.createMany("posts", payload);
|
||||||
expect(res.data.length).toEqual(4);
|
expect(res.data?.length).toEqual(4);
|
||||||
expect(res.ok).toBeTrue();
|
expect(res.ok).toBeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, mock, test } from "bun:test";
|
import { describe, expect, mock, test } from "bun:test";
|
||||||
import type { ModuleBuildContext } from "../../src";
|
import type { ModuleBuildContext } from "../../src";
|
||||||
import { App, createApp } from "../../src/App";
|
import { App, createApp } from "core/test/utils";
|
||||||
import * as proto from "../../src/data/prototype";
|
import * as proto from "../../src/data/prototype";
|
||||||
|
|
||||||
describe("App", () => {
|
describe("App", () => {
|
||||||
@@ -20,6 +20,8 @@ describe("App", () => {
|
|||||||
"guard",
|
"guard",
|
||||||
"flags",
|
"flags",
|
||||||
"logger",
|
"logger",
|
||||||
|
"mcp",
|
||||||
|
"helper",
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -134,4 +136,21 @@ describe("App", () => {
|
|||||||
// expect async listeners to be executed sync after request
|
// expect async listeners to be executed sync after request
|
||||||
expect(called).toHaveBeenCalled();
|
expect(called).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("getMcpClient", async () => {
|
||||||
|
const app = createApp({
|
||||||
|
initialConfig: {
|
||||||
|
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,43 @@
|
|||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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({
|
||||||
|
initialConfig: {
|
||||||
|
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,35 @@
|
|||||||
|
import { describe, it, expect } from "bun:test";
|
||||||
|
import { createApp } from "core/test/utils";
|
||||||
|
import { registries } from "index";
|
||||||
|
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||||
|
|
||||||
|
describe("mcp", () => {
|
||||||
|
it("should have tools", async () => {
|
||||||
|
registries.media.register("local", StorageLocalAdapter);
|
||||||
|
|
||||||
|
const app = createApp({
|
||||||
|
initialConfig: {
|
||||||
|
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({
|
||||||
|
initialConfig: {
|
||||||
|
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({
|
||||||
|
initialConfig: {
|
||||||
|
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,72 @@
|
|||||||
|
import { describe, test, expect, beforeAll, mock, beforeEach, afterAll } from "bun:test";
|
||||||
|
import { type App, createApp, createMcpToolCaller } from "core/test/utils";
|
||||||
|
import type { McpServer } from "bknd/utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* - [x] config_server_get
|
||||||
|
* - [x] config_server_update
|
||||||
|
*/
|
||||||
|
describe("mcp system", async () => {
|
||||||
|
let app: App;
|
||||||
|
let server: McpServer;
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = createApp({
|
||||||
|
initialConfig: {
|
||||||
|
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({
|
||||||
|
initialConfig: {
|
||||||
|
server: {
|
||||||
|
mcp: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await app.build();
|
||||||
|
server = app.mcp!;
|
||||||
|
});
|
||||||
|
|
||||||
|
const tool = createMcpToolCaller();
|
||||||
|
|
||||||
|
test("system_ping", async () => {
|
||||||
|
const result = await tool(server, "system_ping", {});
|
||||||
|
expect(result).toEqual({ pong: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("system_info", async () => {
|
||||||
|
const result = await tool(server, "system_info", {});
|
||||||
|
expect(Object.keys(result).length).toBeGreaterThan(0);
|
||||||
|
expect(Object.keys(result)).toContainValues(["version", "runtime", "connection"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("system_build", async () => {
|
||||||
|
const called = mock(() => null);
|
||||||
|
|
||||||
|
app.emgr.onEvent(AppEvents.AppBuiltEvent, () => void called(), { once: true });
|
||||||
|
|
||||||
|
const result = await tool(server, "system_build", {});
|
||||||
|
expect(called).toHaveBeenCalledTimes(1);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("system_config", async () => {
|
||||||
|
const result = await tool(server, "system_config", {});
|
||||||
|
expect(result).toEqual(app.toJSON());
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { createApp, registries } from "../../src";
|
import { registries } from "../../src";
|
||||||
|
import { createApp } from "core/test/utils";
|
||||||
import * as proto from "../../src/data/prototype";
|
import * as proto from "../../src/data/prototype";
|
||||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||||
|
|
||||||
|
|||||||
@@ -1,46 +1,3 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Authenticator, type User, type UserPool } from "../../src/auth";
|
|
||||||
import { cookieConfig } from "../../src/auth/authenticate/Authenticator";
|
|
||||||
import { PasswordStrategy } from "../../src/auth/authenticate/strategies/PasswordStrategy";
|
|
||||||
import * as hash from "../../src/auth/utils/hash";
|
|
||||||
import { Default, parse } from "../../src/core/utils";
|
|
||||||
|
|
||||||
/*class MemoryUserPool implements UserPool {
|
describe("Authenticator", async () => {});
|
||||||
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);
|
|
||||||
});*/
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Guard } from "../../../src/auth";
|
import { Guard } from "../../../src/auth/authorize/Guard";
|
||||||
|
|
||||||
describe("authorize", () => {
|
describe("authorize", () => {
|
||||||
test("basic", async () => {
|
test("basic", async () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { type TObject, type TString, Type } from "@sinclair/typebox";
|
import { Registry } from "core/registry/Registry";
|
||||||
import { Registry } from "core";
|
import { s } from "core/utils/schema";
|
||||||
|
|
||||||
type Constructor<T> = new (...args: any[]) => T;
|
type Constructor<T> = new (...args: any[]) => T;
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ class What {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
getType() {
|
getType() {
|
||||||
return Type.Object({ type: Type.String() });
|
return s.object({ type: s.string() });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class What2 extends What {}
|
class What2 extends What {}
|
||||||
@@ -19,7 +19,7 @@ class NotAllowed {}
|
|||||||
|
|
||||||
type Test1 = {
|
type Test1 = {
|
||||||
cls: new (...args: any[]) => What;
|
cls: new (...args: any[]) => What;
|
||||||
schema: TObject<{ type: TString }>;
|
schema: s.ObjectSchema<{ type: s.StringSchema }>;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ describe("Registry", () => {
|
|||||||
const registry = new Registry<Test1>().set({
|
const registry = new Registry<Test1>().set({
|
||||||
first: {
|
first: {
|
||||||
cls: What,
|
cls: What,
|
||||||
schema: Type.Object({ type: Type.String(), what: Type.String() }),
|
schema: s.object({ type: s.string(), what: s.string() }),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
} satisfies Record<string, Test1>);
|
} satisfies Record<string, Test1>);
|
||||||
@@ -37,7 +37,7 @@ describe("Registry", () => {
|
|||||||
expect(item).toBeDefined();
|
expect(item).toBeDefined();
|
||||||
expect(item?.cls).toBe(What);
|
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", {
|
registry.add("second", {
|
||||||
cls: What2,
|
cls: What2,
|
||||||
schema: second,
|
schema: second,
|
||||||
@@ -46,7 +46,7 @@ describe("Registry", () => {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(registry.get("second").schema).toEqual(second);
|
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", {
|
registry.add("third", {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
cls: NotAllowed,
|
cls: NotAllowed,
|
||||||
@@ -56,7 +56,7 @@ describe("Registry", () => {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(registry.get("third").schema).toEqual(third);
|
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", {
|
registry.add("fourth", {
|
||||||
cls: What,
|
cls: What,
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
@@ -81,6 +81,8 @@ describe("Registry", () => {
|
|||||||
registry.register("what2", What2);
|
registry.register("what2", What2);
|
||||||
expect(registry.get("what2")).toBeDefined();
|
expect(registry.get("what2")).toBeDefined();
|
||||||
expect(registry.get("what2").cls).toBe(What2);
|
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 { describe, expect, test } from "bun:test";
|
||||||
import { SchemaObject } from "../../../src/core";
|
import { s } from "core/utils/schema";
|
||||||
import { Type } from "@sinclair/typebox";
|
import { SchemaObject } from "core/object/SchemaObject";
|
||||||
|
|
||||||
describe("SchemaObject", async () => {
|
describe("SchemaObject", async () => {
|
||||||
test("basic", async () => {
|
test("basic", async () => {
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
Type.Object({ a: Type.String({ default: "b" }) }),
|
s.strictObject({ a: s.string({ default: "b" }) }),
|
||||||
{ a: "test" },
|
{ a: "test" },
|
||||||
{
|
{
|
||||||
forceParse: true,
|
forceParse: true,
|
||||||
@@ -23,19 +23,19 @@ describe("SchemaObject", async () => {
|
|||||||
|
|
||||||
test("patch", async () => {
|
test("patch", async () => {
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
Type.Object({
|
s.strictObject({
|
||||||
s: Type.Object(
|
s: s.strictObject(
|
||||||
{
|
{
|
||||||
a: Type.String({ default: "b" }),
|
a: s.string({ default: "b" }),
|
||||||
b: Type.Object(
|
b: s.strictObject(
|
||||||
{
|
{
|
||||||
c: Type.String({ default: "d" }),
|
c: s.string({ default: "d" }),
|
||||||
e: Type.String({ default: "f" }),
|
e: s.string({ default: "f" }),
|
||||||
},
|
},
|
||||||
{ default: {} },
|
{ default: {} },
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{ default: {}, additionalProperties: false },
|
{ default: {} },
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -44,7 +44,7 @@ describe("SchemaObject", async () => {
|
|||||||
await m.patch("s.a", "c");
|
await m.patch("s.a", "c");
|
||||||
|
|
||||||
// non-existing path on no additional properties
|
// 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
|
// wrong type
|
||||||
expect(() => m.patch("s.a", 1)).toThrow();
|
expect(() => m.patch("s.a", 1)).toThrow();
|
||||||
|
|
||||||
@@ -58,8 +58,8 @@ describe("SchemaObject", async () => {
|
|||||||
|
|
||||||
test("patch array", async () => {
|
test("patch array", async () => {
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
Type.Object({
|
s.strictObject({
|
||||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
|
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
|
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
|
||||||
@@ -75,13 +75,13 @@ describe("SchemaObject", async () => {
|
|||||||
|
|
||||||
test("remove", async () => {
|
test("remove", async () => {
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
Type.Object({
|
s.object({
|
||||||
s: Type.Object(
|
s: s.object(
|
||||||
{
|
{
|
||||||
a: Type.String({ default: "b" }),
|
a: s.string({ default: "b" }),
|
||||||
b: Type.Object(
|
b: s.object(
|
||||||
{
|
{
|
||||||
c: Type.String({ default: "d" }),
|
c: s.string({ default: "d" }),
|
||||||
},
|
},
|
||||||
{ default: {} },
|
{ default: {} },
|
||||||
),
|
),
|
||||||
@@ -107,8 +107,8 @@ describe("SchemaObject", async () => {
|
|||||||
|
|
||||||
test("set", async () => {
|
test("set", async () => {
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
Type.Object({
|
s.strictObject({
|
||||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
|
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
|
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
|
||||||
@@ -124,8 +124,8 @@ describe("SchemaObject", async () => {
|
|||||||
let called = false;
|
let called = false;
|
||||||
let result: any;
|
let result: any;
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
Type.Object({
|
s.strictObject({
|
||||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
|
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
||||||
}),
|
}),
|
||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
@@ -145,8 +145,8 @@ describe("SchemaObject", async () => {
|
|||||||
test("listener: onBeforeUpdate", async () => {
|
test("listener: onBeforeUpdate", async () => {
|
||||||
let called = false;
|
let called = false;
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
Type.Object({
|
s.strictObject({
|
||||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
|
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
||||||
}),
|
}),
|
||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
@@ -167,7 +167,7 @@ describe("SchemaObject", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("throwIfRestricted", async () => {
|
test("throwIfRestricted", async () => {
|
||||||
const m = new SchemaObject(Type.Object({}), undefined, {
|
const m = new SchemaObject(s.strictObject({}), undefined, {
|
||||||
restrictPaths: ["a.b"],
|
restrictPaths: ["a.b"],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -179,13 +179,13 @@ describe("SchemaObject", async () => {
|
|||||||
|
|
||||||
test("restriction bypass", async () => {
|
test("restriction bypass", async () => {
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
Type.Object({
|
s.strictObject({
|
||||||
s: Type.Object(
|
s: s.strictObject(
|
||||||
{
|
{
|
||||||
a: Type.String({ default: "b" }),
|
a: s.string({ default: "b" }),
|
||||||
b: Type.Object(
|
b: s.strictObject(
|
||||||
{
|
{
|
||||||
c: Type.String({ default: "d" }),
|
c: s.string({ default: "d" }),
|
||||||
},
|
},
|
||||||
{ default: {} },
|
{ default: {} },
|
||||||
),
|
),
|
||||||
@@ -205,7 +205,21 @@ describe("SchemaObject", async () => {
|
|||||||
expect(m.get()).toEqual({ s: { a: "b", b: { c: "e" } } });
|
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(
|
entities: Type.Object(
|
||||||
{},
|
{},
|
||||||
@@ -230,7 +244,7 @@ describe("SchemaObject", async () => {
|
|||||||
{
|
{
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
},
|
},
|
||||||
);
|
); */
|
||||||
test("patch safe object, overwrite", async () => {
|
test("patch safe object, overwrite", async () => {
|
||||||
const data = {
|
const data = {
|
||||||
entities: {
|
entities: {
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
import { Guard } from "../../src/auth";
|
import { Guard } from "../../src/auth/authorize/Guard";
|
||||||
import { parse } from "../../src/core/utils";
|
import { parse } from "core/utils/schema";
|
||||||
import {
|
|
||||||
Entity,
|
|
||||||
type EntityData,
|
|
||||||
EntityManager,
|
|
||||||
ManyToOneRelation,
|
|
||||||
type MutatorResponse,
|
|
||||||
type RepositoryResponse,
|
|
||||||
TextField,
|
|
||||||
} from "../../src/data";
|
|
||||||
import { DataController } from "../../src/data/api/DataController";
|
import { DataController } from "../../src/data/api/DataController";
|
||||||
import { dataConfigSchema } from "../../src/data/data-schema";
|
import { dataConfigSchema } from "../../src/data/data-schema";
|
||||||
import { disableConsoleLog, enableConsoleLog, getDummyConnection } from "../helper";
|
import { disableConsoleLog, enableConsoleLog, getDummyConnection } from "../helper";
|
||||||
|
import type { RepositoryResultJSON } from "data/entities/query/RepositoryResult";
|
||||||
|
import type { MutatorResultJSON } from "data/entities/mutation/MutatorResult";
|
||||||
|
import { Entity, EntityManager, type EntityData } from "data/entities";
|
||||||
|
import { TextField } from "data/fields";
|
||||||
|
import { ManyToOneRelation } from "data/relations";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
beforeAll(() => disableConsoleLog(["log", "warn"]));
|
beforeAll(() => disableConsoleLog(["log", "warn"]));
|
||||||
@@ -21,52 +18,6 @@ afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
|||||||
|
|
||||||
const dataConfig = parse(dataConfigSchema, {});
|
const dataConfig = parse(dataConfigSchema, {});
|
||||||
describe("[data] DataController", async () => {
|
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 () => {
|
describe("getController", async () => {
|
||||||
const users = new Entity("users", [
|
const users = new Entity("users", [
|
||||||
new TextField("name", { required: true }),
|
new TextField("name", { required: true }),
|
||||||
@@ -120,8 +71,7 @@ describe("[data] DataController", async () => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(_user),
|
body: JSON.stringify(_user),
|
||||||
});
|
});
|
||||||
//console.log("res", { _user }, res);
|
const result = (await res.json()) as MutatorResultJSON;
|
||||||
const result = (await res.json()) as MutatorResponse;
|
|
||||||
const { id, ...data } = result.data as any;
|
const { id, ...data } = result.data as any;
|
||||||
|
|
||||||
expect(res.status).toBe(201);
|
expect(res.status).toBe(201);
|
||||||
@@ -135,7 +85,7 @@ describe("[data] DataController", async () => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(_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;
|
const { id, ...data } = result.data as any;
|
||||||
|
|
||||||
expect(res.status).toBe(201);
|
expect(res.status).toBe(201);
|
||||||
@@ -146,13 +96,13 @@ describe("[data] DataController", async () => {
|
|||||||
|
|
||||||
test("/:entity (read many)", async () => {
|
test("/:entity (read many)", async () => {
|
||||||
const res = await app.request("/entity/users");
|
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.total).toBe(3);
|
||||||
expect(data.meta.count).toBe(3);
|
//expect(data.meta.count).toBe(3);
|
||||||
expect(data.meta.items).toBe(3);
|
expect(data.meta.items).toBe(3);
|
||||||
expect(data.data.length).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 () => {
|
test("/:entity/query (func query)", async () => {
|
||||||
@@ -165,33 +115,32 @@ describe("[data] DataController", async () => {
|
|||||||
where: { bio: { $isnull: 1 } },
|
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.total).toBe(3);
|
||||||
expect(data.meta.count).toBe(1);
|
//expect(data.meta.count).toBe(1);
|
||||||
expect(data.meta.items).toBe(1);
|
expect(data.meta.items).toBe(1);
|
||||||
expect(data.data.length).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 () => {
|
test("/:entity (read many, paginated)", async () => {
|
||||||
const res = await app.request("/entity/users?limit=1&offset=2");
|
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.total).toBe(3);
|
||||||
expect(data.meta.count).toBe(3);
|
//expect(data.meta.count).toBe(3);
|
||||||
expect(data.meta.items).toBe(1);
|
expect(data.meta.items).toBe(1);
|
||||||
expect(data.data.length).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 () => {
|
test("/:entity/:id (read one)", async () => {
|
||||||
const res = await app.request("/entity/users/3");
|
const res = await app.request("/entity/users/3");
|
||||||
const data = (await res.json()) as RepositoryResponse<EntityData>;
|
const data = (await res.json()) as RepositoryResultJSON<EntityData>;
|
||||||
console.log("data", data);
|
|
||||||
|
|
||||||
expect(data.meta.total).toBe(3);
|
//expect(data.meta.total).toBe(3);
|
||||||
expect(data.meta.count).toBe(1);
|
//expect(data.meta.count).toBe(1);
|
||||||
expect(data.meta.items).toBe(1);
|
expect(data.meta.items).toBe(1);
|
||||||
expect(data.data).toEqual({ id: 3, ...fixtures.users[2] });
|
expect(data.data).toEqual({ id: 3, ...fixtures.users[2] });
|
||||||
});
|
});
|
||||||
@@ -201,7 +150,7 @@ describe("[data] DataController", async () => {
|
|||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: JSON.stringify({ name: "new name" }),
|
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(res.ok).toBe(true);
|
||||||
expect(data as any).toEqual({ id: 3, ...fixtures.users[2], name: "new name" });
|
expect(data as any).toEqual({ id: 3, ...fixtures.users[2], name: "new name" });
|
||||||
@@ -209,27 +158,26 @@ describe("[data] DataController", async () => {
|
|||||||
|
|
||||||
test("/:entity/:id/:reference (read references)", async () => {
|
test("/:entity/:id/:reference (read references)", async () => {
|
||||||
const res = await app.request("/entity/users/1/posts");
|
const res = await app.request("/entity/users/1/posts");
|
||||||
const data = (await res.json()) as RepositoryResponse;
|
const data = (await res.json()) as RepositoryResultJSON;
|
||||||
console.log("data", data);
|
|
||||||
|
|
||||||
expect(data.meta.total).toBe(2);
|
//expect(data.meta.total).toBe(2);
|
||||||
expect(data.meta.count).toBe(1);
|
//expect(data.meta.count).toBe(1);
|
||||||
expect(data.meta.items).toBe(1);
|
expect(data.meta.items).toBe(1);
|
||||||
expect(data.data.length).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 () => {
|
test("/:entity/:id (delete one)", async () => {
|
||||||
const res = await app.request("/entity/posts/2", {
|
const res = await app.request("/entity/posts/2", {
|
||||||
method: "DELETE",
|
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] });
|
expect(data).toEqual({ id: 2, ...fixtures.posts[1] });
|
||||||
|
|
||||||
// verify
|
// verify
|
||||||
const res2 = await app.request("/entity/posts");
|
const res2 = await app.request("/entity/posts");
|
||||||
const data2 = (await res2.json()) as RepositoryResponse;
|
const data2 = (await res2.json()) as RepositoryResultJSON;
|
||||||
expect(data2.meta.total).toBe(1);
|
//expect(data2.meta.total).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import {
|
import { Entity, EntityManager } from "data/entities";
|
||||||
Entity,
|
import { TextField, PrimaryField, NumberField } from "data/fields";
|
||||||
EntityManager,
|
|
||||||
NumberField,
|
|
||||||
PrimaryField,
|
|
||||||
Repository,
|
|
||||||
TextField,
|
|
||||||
} from "../../src/data";
|
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
@@ -34,19 +28,12 @@ describe("some tests", async () => {
|
|||||||
|
|
||||||
test("findId", async () => {
|
test("findId", async () => {
|
||||||
const query = await em.repository(users).findId(1);
|
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(
|
expect(query.sql).toBe(
|
||||||
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" where "id" = ? limit ?',
|
'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.parameters).toEqual([1, 1]);
|
||||||
expect(query.result).toEqual([]);
|
expect(query.data).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("findMany", async () => {
|
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 ?',
|
'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.parameters).toEqual([10, 0]);
|
||||||
expect(query.result).toEqual([]);
|
expect(query.data).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("findMany with number", async () => {
|
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 ?',
|
'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.parameters).toEqual([10, 0]);
|
||||||
expect(query.result).toEqual([]);
|
expect(query.data).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("try adding an existing field name", async () => {
|
test("try adding an existing field name", async () => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { unlink } from "node:fs/promises";
|
|||||||
import type { SqliteDatabase } from "kysely";
|
import type { SqliteDatabase } from "kysely";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import Database from "libsql";
|
import Database from "libsql";
|
||||||
import { SqliteLocalConnection } from "../../src/data";
|
import { SqliteLocalConnection } from "data/connection/sqlite/SqliteLocalConnection";
|
||||||
|
|
||||||
export function getDummyDatabase(memory: boolean = true): {
|
export function getDummyDatabase(memory: boolean = true): {
|
||||||
dummyDb: SqliteDatabase;
|
dummyDb: SqliteDatabase;
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
// eslint-disable-next-line import/no-unresolved
|
// eslint-disable-next-line import/no-unresolved
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import {
|
import { Entity } from "data/entities";
|
||||||
Entity,
|
import { EntityManager } from "data/entities/EntityManager";
|
||||||
EntityManager,
|
import { ManyToOneRelation } from "data/relations";
|
||||||
ManyToOneRelation,
|
import { NumberField, TextField } from "data/fields";
|
||||||
NumberField,
|
import { SchemaManager } from "data/schema/SchemaManager";
|
||||||
SchemaManager,
|
|
||||||
TextField,
|
|
||||||
} from "../../src/data";
|
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
// eslint-disable-next-line import/no-unresolved
|
// eslint-disable-next-line import/no-unresolved
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { Entity, EntityManager, Mutator, NumberField, TextField } from "../../src/data";
|
import { Entity, EntityManager } from "data/entities";
|
||||||
import { TransformPersistFailedException } from "../../src/data/errors";
|
import { NumberField, TextField } from "data/fields";
|
||||||
|
import { TransformPersistFailedException } from "data/errors";
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
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 () => {
|
test("update inserted row", async () => {
|
||||||
@@ -87,7 +88,7 @@ describe("Mutator simple", async () => {
|
|||||||
expect(mutation.data).toEqual({ id, label: "new label", count: 100 });
|
expect(mutation.data).toEqual({ id, label: "new label", count: 100 });
|
||||||
|
|
||||||
const query2 = await em.repository(items).findId(id);
|
const query2 = await em.repository(items).findId(id);
|
||||||
expect(query2.result.length).toBe(0);
|
expect(query2.data).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("validation: insert incomplete row", async () => {
|
test("validation: insert incomplete row", async () => {
|
||||||
@@ -177,13 +178,13 @@ describe("Mutator simple", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("insertMany", 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 inserts = [{ label: "insert 1" }, { label: "insert 2" }];
|
||||||
const { data } = await em.mutator(items).insertMany(inserts);
|
const { data } = await em.mutator(items).insertMany(inserts);
|
||||||
|
|
||||||
expect(data.length).toBe(2);
|
expect(data.length).toBe(2);
|
||||||
expect(data.map((d) => ({ label: d.label }))).toEqual(inserts);
|
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);
|
expect(newCount).toBe(oldCount + inserts.length);
|
||||||
|
|
||||||
const { data: data2 } = await em.repo(items).findMany({ offset: oldCount });
|
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 { afterAll, expect as bunExpect, describe, test } from "bun:test";
|
||||||
import { stripMark } from "../../src/core/utils";
|
import { stripMark } from "core/utils/schema";
|
||||||
import { Entity, EntityManager, PolymorphicRelation, TextField } from "../../src/data";
|
import { Entity, EntityManager } from "data/entities";
|
||||||
|
import { TextField } from "data/fields";
|
||||||
|
import { PolymorphicRelation } from "data/relations";
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
|
|||||||
@@ -2,19 +2,20 @@ import { describe, expect, test } from "bun:test";
|
|||||||
import {
|
import {
|
||||||
BooleanField,
|
BooleanField,
|
||||||
DateField,
|
DateField,
|
||||||
Entity,
|
|
||||||
EntityIndex,
|
|
||||||
EntityManager,
|
|
||||||
EnumField,
|
EnumField,
|
||||||
JsonField,
|
JsonField,
|
||||||
|
NumberField,
|
||||||
|
TextField,
|
||||||
|
EntityIndex,
|
||||||
|
} from "data/fields";
|
||||||
|
import { Entity, EntityManager } from "data/entities";
|
||||||
|
import {
|
||||||
ManyToManyRelation,
|
ManyToManyRelation,
|
||||||
ManyToOneRelation,
|
ManyToOneRelation,
|
||||||
NumberField,
|
|
||||||
OneToOneRelation,
|
OneToOneRelation,
|
||||||
PolymorphicRelation,
|
PolymorphicRelation,
|
||||||
TextField,
|
} from "data/relations";
|
||||||
} from "../../src/data";
|
import { DummyConnection } from "data/connection/DummyConnection";
|
||||||
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
|
||||||
import {
|
import {
|
||||||
FieldPrototype,
|
FieldPrototype,
|
||||||
type FieldSchema,
|
type FieldSchema,
|
||||||
@@ -32,8 +33,8 @@ import {
|
|||||||
number,
|
number,
|
||||||
relation,
|
relation,
|
||||||
text,
|
text,
|
||||||
} from "../../src/data/prototype";
|
} from "data/prototype";
|
||||||
import { MediaField } from "../../src/media/MediaField";
|
import { MediaField } from "media/MediaField";
|
||||||
|
|
||||||
describe("prototype", () => {
|
describe("prototype", () => {
|
||||||
test("...", () => {
|
test("...", () => {
|
||||||
@@ -101,7 +102,8 @@ describe("prototype", () => {
|
|||||||
|
|
||||||
type Posts = Schema<typeof posts2>;
|
type Posts = Schema<typeof posts2>;
|
||||||
|
|
||||||
expect(posts1.toJSON()).toEqual(posts2.toJSON());
|
// @todo: check
|
||||||
|
//expect(posts1.toJSON()).toEqual(posts2.toJSON());
|
||||||
});
|
});
|
||||||
|
|
||||||
test("test example", async () => {
|
test("test example", async () => {
|
||||||
@@ -295,9 +297,9 @@ describe("prototype", () => {
|
|||||||
new Entity("posts", [new TextField("name"), new TextField("slug", { required: true })]),
|
new Entity("posts", [new TextField("name"), new TextField("slug", { required: true })]),
|
||||||
new Entity("comments", [new TextField("some")]),
|
new Entity("comments", [new TextField("some")]),
|
||||||
new Entity("users", [new TextField("email")]),
|
new Entity("users", [new TextField("email")]),
|
||||||
];
|
] as const;
|
||||||
const _em2 = new EntityManager(
|
const _em2 = new EntityManager(
|
||||||
es,
|
[...es],
|
||||||
new DummyConnection(),
|
new DummyConnection(),
|
||||||
[new ManyToOneRelation(es[0], es[1]), new ManyToOneRelation(es[0], es[2])],
|
[new ManyToOneRelation(es[0], es[1]), new ManyToOneRelation(es[0], es[2])],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
// eslint-disable-next-line import/no-unresolved
|
// eslint-disable-next-line import/no-unresolved
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { Entity, EntityManager, TextField } from "../../src/data";
|
import { Entity, EntityManager } from "data/entities";
|
||||||
|
import { TextField } from "data/fields";
|
||||||
import {
|
import {
|
||||||
ManyToManyRelation,
|
ManyToManyRelation,
|
||||||
ManyToOneRelation,
|
ManyToOneRelation,
|
||||||
OneToOneRelation,
|
OneToOneRelation,
|
||||||
PolymorphicRelation,
|
PolymorphicRelation,
|
||||||
RelationField,
|
RelationField,
|
||||||
} from "../../src/data/relations";
|
} from "data/relations";
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
@@ -77,7 +78,7 @@ describe("Relations", async () => {
|
|||||||
const em = new EntityManager(entities, dummyConnection, relations);
|
const em = new EntityManager(entities, dummyConnection, relations);
|
||||||
|
|
||||||
// verify naming
|
// verify naming
|
||||||
const rel = em.relations.all[0];
|
const rel = em.relations.all[0]!;
|
||||||
expect(rel.source.entity.name).toBe(posts.name);
|
expect(rel.source.entity.name).toBe(posts.name);
|
||||||
expect(rel.source.reference).toBe(posts.name);
|
expect(rel.source.reference).toBe(posts.name);
|
||||||
expect(rel.target.entity.name).toBe(users.name);
|
expect(rel.target.entity.name).toBe(users.name);
|
||||||
@@ -89,11 +90,11 @@ describe("Relations", async () => {
|
|||||||
// verify low level relation
|
// verify low level relation
|
||||||
expect(em.relationsOf(users.name).length).toBe(1);
|
expect(em.relationsOf(users.name).length).toBe(1);
|
||||||
expect(em.relationsOf(users.name).length).toBe(1);
|
expect(em.relationsOf(users.name).length).toBe(1);
|
||||||
expect(em.relationsOf(users.name)[0].source.entity).toBe(posts);
|
expect(em.relationsOf(users.name)[0]!.source.entity).toBe(posts);
|
||||||
expect(posts.field("author_id")).toBeInstanceOf(RelationField);
|
expect(posts.field("author_id")).toBeInstanceOf(RelationField);
|
||||||
expect(em.relationsOf(users.name).length).toBe(1);
|
expect(em.relationsOf(users.name).length).toBe(1);
|
||||||
expect(em.relationsOf(users.name).length).toBe(1);
|
expect(em.relationsOf(users.name).length).toBe(1);
|
||||||
expect(em.relationsOf(users.name)[0].source.entity).toBe(posts);
|
expect(em.relationsOf(users.name)[0]!.source.entity).toBe(posts);
|
||||||
|
|
||||||
// verify high level relation (from users)
|
// verify high level relation (from users)
|
||||||
const userPostsRel = em.relationOf(users.name, "posts");
|
const userPostsRel = em.relationOf(users.name, "posts");
|
||||||
@@ -191,7 +192,7 @@ describe("Relations", async () => {
|
|||||||
const em = new EntityManager(entities, dummyConnection, relations);
|
const em = new EntityManager(entities, dummyConnection, relations);
|
||||||
|
|
||||||
// verify naming
|
// verify naming
|
||||||
const rel = em.relations.all[0];
|
const rel = em.relations.all[0]!;
|
||||||
expect(rel.source.entity.name).toBe(users.name);
|
expect(rel.source.entity.name).toBe(users.name);
|
||||||
expect(rel.source.reference).toBe(users.name);
|
expect(rel.source.reference).toBe(users.name);
|
||||||
expect(rel.target.entity.name).toBe(settings.name);
|
expect(rel.target.entity.name).toBe(settings.name);
|
||||||
@@ -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).length).toBe(1);
|
expect(em.relationsOf(users.name).length).toBe(1);
|
||||||
expect(em.relationsOf(users.name)[0].source.entity).toBe(users);
|
expect(em.relationsOf(users.name)[0]!.source.entity).toBe(users);
|
||||||
expect(em.relationsOf(users.name)[0].target.entity).toBe(settings);
|
expect(em.relationsOf(users.name)[0]!.target.entity).toBe(settings);
|
||||||
|
|
||||||
// verify high level relation (from users)
|
// verify high level relation (from users)
|
||||||
const userSettingRel = em.relationOf(users.name, settings.name);
|
const userSettingRel = em.relationOf(users.name, settings.name);
|
||||||
@@ -323,7 +324,7 @@ describe("Relations", async () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// mutation info
|
// mutation info
|
||||||
expect(relations[0].helper(posts.name)!.getMutationInfo()).toEqual({
|
expect(relations[0]!.helper(posts.name)!.getMutationInfo()).toEqual({
|
||||||
reference: "categories",
|
reference: "categories",
|
||||||
local_field: undefined,
|
local_field: undefined,
|
||||||
$set: false,
|
$set: false,
|
||||||
@@ -334,7 +335,7 @@ describe("Relations", async () => {
|
|||||||
cardinality: undefined,
|
cardinality: undefined,
|
||||||
relation_type: "m:n",
|
relation_type: "m:n",
|
||||||
});
|
});
|
||||||
expect(relations[0].helper(categories.name)!.getMutationInfo()).toEqual({
|
expect(relations[0]!.helper(categories.name)!.getMutationInfo()).toEqual({
|
||||||
reference: "posts",
|
reference: "posts",
|
||||||
local_field: undefined,
|
local_field: undefined,
|
||||||
$set: false,
|
$set: false,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Entity, NumberField, TextField } from "data";
|
import { Entity } from "data/entities";
|
||||||
import * as p from "data/prototype";
|
import { NumberField, TextField } from "data/fields";
|
||||||
|
|
||||||
describe("[data] Entity", async () => {
|
describe("[data] Entity", async () => {
|
||||||
const entity = new Entity("test", [
|
const entity = new Entity("test", [
|
||||||
@@ -47,8 +47,4 @@ describe("[data] Entity", async () => {
|
|||||||
entity.addField(field);
|
entity.addField(field);
|
||||||
expect(entity.getField("new_field")).toBe(field);
|
expect(entity.getField("new_field")).toBe(field);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.only("types", async () => {
|
|
||||||
console.log(entity.toTypes());
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import {
|
import { Entity, EntityManager } from "data/entities";
|
||||||
Entity,
|
import { ManyToManyRelation, ManyToOneRelation } from "data/relations";
|
||||||
EntityManager,
|
import { SchemaManager } from "data/schema/SchemaManager";
|
||||||
ManyToManyRelation,
|
import { UnableToConnectException } from "data/errors";
|
||||||
ManyToOneRelation,
|
|
||||||
SchemaManager,
|
|
||||||
} from "../../../src/data";
|
|
||||||
import { UnableToConnectException } from "../../../src/data/errors";
|
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { Entity, EntityManager, ManyToOneRelation, TextField } from "../../../src/data";
|
import { Entity, EntityManager } from "data/entities";
|
||||||
import { JoinBuilder } from "../../../src/data/entities/query/JoinBuilder";
|
import { ManyToOneRelation } from "data/relations";
|
||||||
|
import { TextField } from "data/fields";
|
||||||
|
import { JoinBuilder } from "data/entities/query/JoinBuilder";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
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 type { EventManager } from "../../../src/core/events";
|
||||||
|
import { Entity, EntityManager } from "data/entities";
|
||||||
import {
|
import {
|
||||||
Entity,
|
|
||||||
EntityManager,
|
|
||||||
ManyToOneRelation,
|
ManyToOneRelation,
|
||||||
MutatorEvents,
|
|
||||||
NumberField,
|
|
||||||
OneToOneRelation,
|
OneToOneRelation,
|
||||||
type RelationField,
|
RelationField,
|
||||||
RelationMutator,
|
RelationMutator,
|
||||||
TextField,
|
} from "data/relations";
|
||||||
} from "../../../src/data";
|
import { NumberField, TextField } from "data/fields";
|
||||||
import * as proto from "../../../src/data/prototype";
|
import * as proto from "data/prototype";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection, disableConsoleLog, enableConsoleLog } from "../../helper";
|
||||||
|
import { MutatorEvents } from "data/events";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
afterAll(afterAllCleanup);
|
afterAll(afterAllCleanup);
|
||||||
|
|
||||||
|
beforeAll(() => disableConsoleLog(["log", "warn"]));
|
||||||
|
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||||
|
|
||||||
describe("[data] Mutator (base)", async () => {
|
describe("[data] Mutator (base)", async () => {
|
||||||
const entity = new Entity("items", [
|
const entity = new Entity("items", [
|
||||||
new TextField("label", { required: true }),
|
new TextField("label", { required: true }),
|
||||||
|
|||||||
@@ -1,17 +1,10 @@
|
|||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import type { Kysely, Transaction } from "kysely";
|
import type { Kysely, Transaction } from "kysely";
|
||||||
import { Perf } from "core/utils";
|
import { TextField } from "data/fields";
|
||||||
import {
|
import { em as $em, entity as $entity, text as $text } from "data/prototype";
|
||||||
Entity,
|
import { Entity, EntityManager } from "data/entities";
|
||||||
EntityManager,
|
import { ManyToOneRelation } from "data/relations";
|
||||||
LibsqlConnection,
|
import { RepositoryEvents } from "data/events";
|
||||||
ManyToOneRelation,
|
|
||||||
RepositoryEvents,
|
|
||||||
TextField,
|
|
||||||
entity as $entity,
|
|
||||||
text as $text,
|
|
||||||
em as $em,
|
|
||||||
} from "data";
|
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
|
|
||||||
type E = Kysely<any> | Transaction<any>;
|
type E = Kysely<any> | Transaction<any>;
|
||||||
@@ -26,120 +19,6 @@ async function sleep(ms: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("[Repository]", async () => {
|
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 () => {
|
test("count & exists", async () => {
|
||||||
const items = new Entity("items", [new TextField("label")]);
|
const items = new Entity("items", [new TextField("label")]);
|
||||||
const em = new EntityManager([items], dummyConnection);
|
const em = new EntityManager([items], dummyConnection);
|
||||||
@@ -160,25 +39,44 @@ describe("[Repository]", async () => {
|
|||||||
// count all
|
// count all
|
||||||
const res = await em.repository(items).count();
|
const res = await em.repository(items).count();
|
||||||
expect(res.sql).toBe('select count(*) as "count" from "items"');
|
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
|
// 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.sql).toBe('select count(*) as "count" from "items" where "label" in (?, ?)');
|
||||||
expect(res2.parameters).toEqual(["a", "b"]);
|
expect(res2.parameters).toEqual(["a", "b"]);
|
||||||
expect(res2.count).toBe(2);
|
expect(res2.data.count).toBe(2);
|
||||||
|
|
||||||
// check exists
|
// check exists
|
||||||
const res3 = await em.repository(items).exists({ label: "a" });
|
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" });
|
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
|
// for now, allow empty filter
|
||||||
const res5 = await em.repository(items).exists({});
|
const res5 = await em.repository(items).exists({});
|
||||||
expect(res5.exists).toBe(true);
|
expect(res5.data.exists).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("option: silent", async () => {
|
test("option: silent", async () => {
|
||||||
@@ -191,6 +89,9 @@ describe("[Repository]", async () => {
|
|||||||
// should throw because table doesn't exist
|
// should throw because table doesn't exist
|
||||||
expect(em.repo("items").findMany({})).rejects.toThrow(/no such table/);
|
expect(em.repo("items").findMany({})).rejects.toThrow(/no such table/);
|
||||||
// should silently return empty result
|
// should silently return empty result
|
||||||
|
em.repo("items", { silent: true })
|
||||||
|
.findMany({})
|
||||||
|
.then((r) => r.data);
|
||||||
expect(
|
expect(
|
||||||
em
|
em
|
||||||
.repo("items", { silent: true })
|
.repo("items", { silent: true })
|
||||||
@@ -209,16 +110,16 @@ describe("[Repository]", async () => {
|
|||||||
|
|
||||||
expect(
|
expect(
|
||||||
em
|
em
|
||||||
.repo("items")
|
.repo("items", { includeCounts: true })
|
||||||
.findMany({})
|
.findMany({})
|
||||||
.then((r) => [r.meta.count, r.meta.total]),
|
.then((r) => [r.count, r.total]),
|
||||||
).resolves.toEqual([0, 0]);
|
).resolves.toEqual([0, 0]);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
em
|
em
|
||||||
.repo("items", { includeCounts: false })
|
.repo("items", { includeCounts: false })
|
||||||
.findMany({})
|
.findMany({})
|
||||||
.then((r) => [r.meta.count, r.meta.total]),
|
.then((r) => [r.count, r.total]),
|
||||||
).resolves.toEqual([undefined, undefined]);
|
).resolves.toEqual([undefined, undefined]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
// eslint-disable-next-line import/no-unresolved
|
// eslint-disable-next-line import/no-unresolved
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { randomString } from "../../../src/core/utils";
|
import { randomString } from "core/utils";
|
||||||
import { Entity, EntityIndex, EntityManager, SchemaManager, TextField } from "../../../src/data";
|
import { Entity, EntityManager } from "data/entities";
|
||||||
|
import { TextField, EntityIndex } from "data/fields";
|
||||||
|
import { SchemaManager } from "data/schema/SchemaManager";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, test, expect } from "bun:test";
|
import { describe, test, expect } from "bun:test";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
import { type WhereQuery, WhereBuilder } from "data";
|
import { WhereBuilder, type WhereQuery } from "data/entities/query/WhereBuilder";
|
||||||
|
|
||||||
function qb() {
|
function qb() {
|
||||||
const c = getDummyConnection();
|
const c = getDummyConnection();
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import {
|
import { Entity, EntityManager } from "data/entities";
|
||||||
Entity,
|
import { ManyToManyRelation, ManyToOneRelation, PolymorphicRelation } from "data/relations";
|
||||||
EntityManager,
|
import { TextField } from "data/fields";
|
||||||
ManyToManyRelation,
|
import * as proto from "data/prototype";
|
||||||
ManyToOneRelation,
|
import { WithBuilder } from "data/entities/query/WithBuilder";
|
||||||
PolymorphicRelation,
|
|
||||||
TextField,
|
|
||||||
WithBuilder,
|
|
||||||
} from "../../../src/data";
|
|
||||||
import * as proto from "../../../src/data/prototype";
|
|
||||||
import { schemaToEm } from "../../helper";
|
import { schemaToEm } from "../../helper";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
|
|
||||||
@@ -89,9 +84,9 @@ describe("[data] WithBuilder", async () => {
|
|||||||
const res2 = qb2.compile();
|
const res2 = qb2.compile();
|
||||||
|
|
||||||
expect(res2.sql).toBe(
|
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 () => {
|
test("test with empty join", async () => {
|
||||||
@@ -194,9 +189,9 @@ describe("[data] WithBuilder", async () => {
|
|||||||
);
|
);
|
||||||
const res = qb.compile();
|
const res = qb.compile();
|
||||||
expect(res.sql).toBe(
|
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(
|
const qb2 = WithBuilder.addClause(
|
||||||
em,
|
em,
|
||||||
@@ -273,9 +268,9 @@ describe("[data] WithBuilder", async () => {
|
|||||||
|
|
||||||
//prettyPrintQb(qb);
|
//prettyPrintQb(qb);
|
||||||
expect(qb.compile().sql).toBe(
|
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 () => {
|
test("compiles with many", async () => {
|
||||||
@@ -315,9 +310,9 @@ describe("[data] WithBuilder", async () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(qb.compile().sql).toBe(
|
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 () => {
|
test("returns correct result", async () => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { EntityManager } from "../../../../src/data";
|
import { EntityManager } from "data/entities/EntityManager";
|
||||||
import { getDummyConnection } from "../../helper";
|
import { getDummyConnection } from "../../helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
import { describe, expect, test } from "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";
|
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
describe("[data] BooleanField", async () => {
|
describe("[data] BooleanField", async () => {
|
||||||
fieldTestSuite({ expect, test }, BooleanField, { defaultValue: true, schemaType: "boolean" });
|
fieldTestSuite(bunTestRunner, BooleanField, { defaultValue: true, schemaType: "boolean" });
|
||||||
|
|
||||||
test("transformRetrieve", async () => {
|
test("transformRetrieve", async () => {
|
||||||
const field = new BooleanField("test");
|
const field = new BooleanField("test");
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, test } from "bun:test";
|
||||||
import { DateField } from "../../../../src/data";
|
import { DateField } from "data/fields";
|
||||||
import { fieldTestSuite } from "data/fields/field-test-suite";
|
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||||
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
|
|
||||||
describe("[data] DateField", async () => {
|
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
|
// @todo: add datefield tests
|
||||||
test("week", async () => {
|
test("week", async () => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
import { describe, expect, test } from "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";
|
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
function options(strings: string[]) {
|
function options(strings: string[]) {
|
||||||
@@ -8,7 +9,7 @@ function options(strings: string[]) {
|
|||||||
|
|
||||||
describe("[data] EnumField", async () => {
|
describe("[data] EnumField", async () => {
|
||||||
fieldTestSuite(
|
fieldTestSuite(
|
||||||
{ expect, test },
|
bunTestRunner,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
EnumField,
|
EnumField,
|
||||||
{ defaultValue: "a", schemaType: "text" },
|
{ defaultValue: "a", schemaType: "text" },
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Default, stripMark } from "../../../../src/core/utils";
|
|
||||||
import { baseFieldConfigSchema, Field } from "../../../../src/data/fields/Field";
|
import { baseFieldConfigSchema, Field } from "../../../../src/data/fields/Field";
|
||||||
import { fieldTestSuite } from "data/fields/field-test-suite";
|
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||||
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
|
import { stripMark } from "core/utils/schema";
|
||||||
|
|
||||||
describe("[data] Field", async () => {
|
describe("[data] Field", async () => {
|
||||||
class FieldSpec extends Field {
|
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 () => {
|
test("default config", async () => {
|
||||||
const config = Default(baseFieldConfigSchema, {});
|
const config = baseFieldConfigSchema.template({});
|
||||||
expect(stripMark(new FieldSpec("test").config)).toEqual(config as any);
|
expect(stripMark(new FieldSpec("test").config)).toEqual(config as any);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Type } from "@sinclair/typebox";
|
import { Entity } from "data/entities";
|
||||||
import { Entity, EntityIndex, Field } from "../../../../src/data";
|
import { Field, EntityIndex } from "data/fields";
|
||||||
|
import { s } from "core/utils/schema";
|
||||||
|
|
||||||
class TestField extends Field {
|
class TestField extends Field {
|
||||||
protected getSchema(): any {
|
protected getSchema(): any {
|
||||||
return Type.Any();
|
return s.any();
|
||||||
}
|
}
|
||||||
|
|
||||||
override schema() {
|
override schema() {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
import { describe, expect, test } from "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";
|
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
describe("[data] JsonField", async () => {
|
describe("[data] JsonField", async () => {
|
||||||
const field = new JsonField("test");
|
const field = new JsonField("test");
|
||||||
fieldTestSuite({ expect, test }, JsonField, {
|
fieldTestSuite(bunTestRunner, JsonField, {
|
||||||
defaultValue: { a: 1 },
|
defaultValue: { a: 1 },
|
||||||
sampleValues: ["string", { test: 1 }, 1],
|
sampleValues: ["string", { test: 1 }, 1],
|
||||||
schemaType: "text",
|
schemaType: "text",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { JsonSchemaField } from "../../../../src/data";
|
import { JsonSchemaField } from "data/fields";
|
||||||
import { fieldTestSuite } from "data/fields/field-test-suite";
|
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
describe("[data] JsonSchemaField", async () => {
|
describe("[data] JsonSchemaField", async () => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
import { describe, expect, test } from "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";
|
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
describe("[data] NumberField", async () => {
|
describe("[data] NumberField", async () => {
|
||||||
@@ -15,5 +16,5 @@ describe("[data] NumberField", async () => {
|
|||||||
expect(transformPersist(field2, 10000)).resolves.toBe(10000);
|
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 { describe, expect, test } from "bun:test";
|
||||||
import { PrimaryField } from "../../../../src/data";
|
import { PrimaryField } from "data/fields";
|
||||||
|
|
||||||
describe("[data] PrimaryField", async () => {
|
describe("[data] PrimaryField", async () => {
|
||||||
const field = new PrimaryField("primary");
|
const field = new PrimaryField("primary");
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
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 { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
|
|
||||||
describe("[data] TextField", async () => {
|
describe("[data] TextField", async () => {
|
||||||
test("transformPersist (config)", async () => {
|
test("transformPersist (config)", async () => {
|
||||||
@@ -11,5 +12,5 @@ describe("[data] TextField", async () => {
|
|||||||
expect(transformPersist(field, "abc")).resolves.toBe("abc");
|
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,11 @@
|
|||||||
import { describe, expect, it, test } from "bun:test";
|
import { describe, expect, it, test } from "bun:test";
|
||||||
import { Entity, type EntityManager } from "../../../../src/data";
|
import { Entity, type EntityManager } from "data/entities";
|
||||||
import {
|
import {
|
||||||
type BaseRelationConfig,
|
type BaseRelationConfig,
|
||||||
EntityRelation,
|
EntityRelation,
|
||||||
EntityRelationAnchor,
|
EntityRelationAnchor,
|
||||||
RelationTypes,
|
RelationTypes,
|
||||||
} from "../../../../src/data/relations";
|
} from "data/relations";
|
||||||
|
|
||||||
class TestEntityRelation extends EntityRelation {
|
class TestEntityRelation extends EntityRelation {
|
||||||
constructor(config?: BaseRelationConfig) {
|
constructor(config?: BaseRelationConfig) {
|
||||||
@@ -24,11 +24,11 @@ class TestEntityRelation extends EntityRelation {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildWith(a: any, b: any, c: any): any {
|
buildWith(): any {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildJoin(a: any, b: any): any {
|
buildJoin(): any {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
afterAll(unmockFetch);
|
||||||
|
|
||||||
describe("FetchTask", async () => {
|
describe.skip("FetchTask", async () => {
|
||||||
test("Simple fetch", async () => {
|
test("Simple fetch", async () => {
|
||||||
const task = new FetchTask("Fetch Something", {
|
const task = new FetchTask("Fetch Something", {
|
||||||
url: "https://jsonplaceholder.typicode.com/todos/1",
|
url: "https://jsonplaceholder.typicode.com/todos/1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Flow, LogTask, SubFlowTask, RenderTask, Task } from "../../src/flows";
|
import { Flow, LogTask, SubFlowTask, RenderTask, Task } from "../../src/flows";
|
||||||
import { Type } from "@sinclair/typebox";
|
import { s } from "core/utils/schema";
|
||||||
|
|
||||||
export class StringifyTask<Output extends string> extends Task<
|
export class StringifyTask<Output extends string> extends Task<
|
||||||
typeof StringifyTask.schema,
|
typeof StringifyTask.schema,
|
||||||
@@ -8,18 +8,16 @@ export class StringifyTask<Output extends string> extends Task<
|
|||||||
> {
|
> {
|
||||||
type = "stringify";
|
type = "stringify";
|
||||||
|
|
||||||
static override schema = Type.Optional(
|
static override schema = s.object({
|
||||||
Type.Object({
|
input: s.string().optional(),
|
||||||
input: Type.Optional(Type.String()),
|
});
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
async execute() {
|
async execute() {
|
||||||
return JSON.stringify(this.params.input) as Output;
|
return JSON.stringify(this.params.input) as Output;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("SubFlowTask", async () => {
|
describe.skip("SubFlowTask", async () => {
|
||||||
test("Simple Subflow", async () => {
|
test("Simple Subflow", async () => {
|
||||||
const subTask = new RenderTask("render", {
|
const subTask = new RenderTask("render", {
|
||||||
render: "subflow",
|
render: "subflow",
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Type } from "@sinclair/typebox";
|
|
||||||
import { Task } from "../../src/flows";
|
import { Task } from "../../src/flows";
|
||||||
import { dynamic } from "../../src/flows/tasks/Task";
|
import { dynamic } from "../../src/flows/tasks/Task";
|
||||||
|
import { s } from "core/utils/schema";
|
||||||
|
|
||||||
describe("Task", async () => {
|
describe.skip("Task", async () => {
|
||||||
test("resolveParams: template with parse", async () => {
|
test("resolveParams: template with parse", async () => {
|
||||||
const result = await Task.resolveParams(
|
const result = await Task.resolveParams(
|
||||||
Type.Object({ test: dynamic(Type.Number()) }),
|
s.object({ test: dynamic(s.number()) }),
|
||||||
{
|
{
|
||||||
test: "{{ some.path }}",
|
test: "{{ some.path }}",
|
||||||
},
|
},
|
||||||
@@ -22,7 +22,7 @@ describe("Task", async () => {
|
|||||||
|
|
||||||
test("resolveParams: with string", async () => {
|
test("resolveParams: with string", async () => {
|
||||||
const result = await Task.resolveParams(
|
const result = await Task.resolveParams(
|
||||||
Type.Object({ test: Type.String() }),
|
s.object({ test: s.string() }),
|
||||||
{
|
{
|
||||||
test: "{{ some.path }}",
|
test: "{{ some.path }}",
|
||||||
},
|
},
|
||||||
@@ -38,7 +38,7 @@ describe("Task", async () => {
|
|||||||
|
|
||||||
test("resolveParams: with object", async () => {
|
test("resolveParams: with object", async () => {
|
||||||
const result = await Task.resolveParams(
|
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 }}" },
|
test: { key: "path", value: "{{ some.path }}" },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { Event, EventManager } from "../../src/core/events";
|
import { Event, EventManager } from "../../src/core/events";
|
||||||
import { parse } from "../../src/core/utils";
|
import { s, parse } from "core/utils/schema";
|
||||||
import { type Static, type StaticDecode, Type } from "@sinclair/typebox";
|
|
||||||
import { EventTrigger, Flow, HttpTrigger, type InputsMap, Task } from "../../src/flows";
|
import { EventTrigger, Flow, HttpTrigger, type InputsMap, Task } from "../../src/flows";
|
||||||
import { dynamic } from "../../src/flows/tasks/Task";
|
import { dynamic } from "../../src/flows/tasks/Task";
|
||||||
|
|
||||||
@@ -15,15 +14,15 @@ class Passthrough extends Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type OutputIn = Static<typeof OutputParamTask.schema>;
|
type OutputIn = s.Static<typeof OutputParamTask.schema>;
|
||||||
type OutputOut = StaticDecode<typeof OutputParamTask.schema>;
|
type OutputOut = s.StaticCoerced<typeof OutputParamTask.schema>;
|
||||||
|
|
||||||
class OutputParamTask extends Task<typeof OutputParamTask.schema> {
|
class OutputParamTask extends Task<typeof OutputParamTask.schema> {
|
||||||
type = "output-param";
|
type = "output-param";
|
||||||
|
|
||||||
static override schema = Type.Object({
|
static override schema = s.strictObject({
|
||||||
number: dynamic(
|
number: dynamic(
|
||||||
Type.Number({
|
s.number({
|
||||||
title: "Output number",
|
title: "Output number",
|
||||||
}),
|
}),
|
||||||
Number.parseInt,
|
Number.parseInt,
|
||||||
@@ -44,7 +43,7 @@ class PassthroughFlowInput extends Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Flow task inputs", async () => {
|
describe.skip("Flow task inputs", async () => {
|
||||||
test("types", async () => {
|
test("types", async () => {
|
||||||
const schema = OutputParamTask.schema;
|
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 () => {
|
test("manual trigger", async () => {
|
||||||
let called = false;
|
let called = false;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { isEqual } from "lodash-es";
|
import { isEqual } from "lodash-es";
|
||||||
import { _jsonp, withDisabledConsole } from "../../src/core/utils";
|
import { _jsonp, withDisabledConsole } from "../../src/core/utils";
|
||||||
import { type Static, Type } from "@sinclair/typebox";
|
import { s } from "core/utils/schema";
|
||||||
import { Condition, ExecutionEvent, FetchTask, Flow, LogTask, Task } from "../../src/flows";
|
import { Condition, ExecutionEvent, FetchTask, Flow, LogTask, Task } from "../../src/flows";
|
||||||
|
|
||||||
/*beforeAll(disableConsoleLog);
|
/*beforeAll(disableConsoleLog);
|
||||||
@@ -11,19 +11,19 @@ afterAll(enableConsoleLog);*/
|
|||||||
class ExecTask extends Task<typeof ExecTask.schema> {
|
class ExecTask extends Task<typeof ExecTask.schema> {
|
||||||
type = "exec";
|
type = "exec";
|
||||||
|
|
||||||
static override schema = Type.Object({
|
static override schema = s.object({
|
||||||
delay: Type.Number({ default: 10 }),
|
delay: s.number({ default: 10 }),
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
name: string,
|
name: string,
|
||||||
params: Static<typeof ExecTask.schema>,
|
params: s.Static<typeof ExecTask.schema>,
|
||||||
private func: () => Promise<any>,
|
private func: () => Promise<any>,
|
||||||
) {
|
) {
|
||||||
super(name, params);
|
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);
|
return new ExecTask(name, params, this.func);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ function getObjectDiff(obj1, obj2) {
|
|||||||
return diff;
|
return diff;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Flow tests", async () => {
|
describe.skip("Flow tests", async () => {
|
||||||
test("Simple single task", async () => {
|
test("Simple single task", async () => {
|
||||||
const simple = getTask(0);
|
const simple = getTask(0);
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import { unlink } from "node:fs/promises";
|
|||||||
import type { SelectQueryBuilder, SqliteDatabase } from "kysely";
|
import type { SelectQueryBuilder, SqliteDatabase } from "kysely";
|
||||||
import Database from "libsql";
|
import Database from "libsql";
|
||||||
import { format as sqlFormat } from "sql-formatter";
|
import { format as sqlFormat } from "sql-formatter";
|
||||||
import { type Connection, EntityManager, SqliteLocalConnection } from "../src/data";
|
|
||||||
import type { em as protoEm } from "../src/data/prototype";
|
import type { em as protoEm } from "../src/data/prototype";
|
||||||
import { writeFile } from "node:fs/promises";
|
import { writeFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { slugify } from "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): {
|
export function getDummyDatabase(memory: boolean = true): {
|
||||||
dummyDb: SqliteDatabase;
|
dummyDb: SqliteDatabase;
|
||||||
@@ -38,14 +39,15 @@ export function getLocalLibsqlConnection() {
|
|||||||
return { url: "http://127.0.0.1:8080" };
|
return { url: "http://127.0.0.1:8080" };
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConsoleSeverity = "log" | "warn" | "error";
|
type ConsoleSeverity = "debug" | "log" | "warn" | "error";
|
||||||
const _oldConsoles = {
|
const _oldConsoles = {
|
||||||
|
debug: console.debug,
|
||||||
log: console.log,
|
log: console.log,
|
||||||
warn: console.warn,
|
warn: console.warn,
|
||||||
error: console.error,
|
error: console.error,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function disableConsoleLog(severities: ConsoleSeverity[] = ["log", "warn"]) {
|
export function disableConsoleLog(severities: ConsoleSeverity[] = ["debug", "log", "warn"]) {
|
||||||
severities.forEach((severity) => {
|
severities.forEach((severity) => {
|
||||||
console[severity] = () => null;
|
console[severity] = () => null;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from "bun:test";
|
import { describe, expect, it } from "bun:test";
|
||||||
import { createApp } from "../../src";
|
import { createApp } from "core/test/utils";
|
||||||
import { Api } from "../../src/Api";
|
import { Api } from "../../src/Api";
|
||||||
|
|
||||||
describe("integration config", () => {
|
describe("integration config", () => {
|
||||||
@@ -13,9 +13,8 @@ describe("integration config", () => {
|
|||||||
|
|
||||||
// create entity
|
// create entity
|
||||||
await api.system.addConfig("data", "entities.posts", {
|
await api.system.addConfig("data", "entities.posts", {
|
||||||
name: "posts",
|
|
||||||
config: { sort_field: "id", sort_dir: "asc" },
|
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",
|
type: "regular",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
/// <reference types="@types/bun" />
|
/// <reference types="@types/bun" />
|
||||||
|
|
||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import { createApp, registries } from "../../src";
|
import { registries } from "../../src";
|
||||||
|
import { createApp } from "core/test/utils";
|
||||||
import { mergeObject, randomString } from "../../src/core/utils";
|
import { mergeObject, randomString } from "../../src/core/utils";
|
||||||
import type { TAppMediaConfig } from "../../src/media/media-schema";
|
import type { TAppMediaConfig } from "../../src/media/media-schema";
|
||||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||||
@@ -45,7 +46,6 @@ afterAll(enableConsoleLog);
|
|||||||
describe("MediaController", () => {
|
describe("MediaController", () => {
|
||||||
test("accepts direct", async () => {
|
test("accepts direct", async () => {
|
||||||
const app = await makeApp();
|
const app = await makeApp();
|
||||||
console.log("app", app);
|
|
||||||
|
|
||||||
const file = Bun.file(path);
|
const file = Bun.file(path);
|
||||||
const name = makeName("png");
|
const name = makeName("png");
|
||||||
@@ -54,7 +54,6 @@ describe("MediaController", () => {
|
|||||||
body: file,
|
body: file,
|
||||||
});
|
});
|
||||||
const result = (await res.json()) as any;
|
const result = (await res.json()) as any;
|
||||||
console.log(result);
|
|
||||||
expect(result.name).toBe(name);
|
expect(result.name).toBe(name);
|
||||||
|
|
||||||
const destFile = Bun.file(assetsTmpPath + "/" + name);
|
const destFile = Bun.file(assetsTmpPath + "/" + name);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { type FileBody, Storage } from "../../src/media/storage/Storage";
|
import { type FileBody, Storage } from "../../src/media/storage/Storage";
|
||||||
import * as StorageEvents from "../../src/media/storage/events";
|
import * as StorageEvents from "../../src/media/storage/events";
|
||||||
import { StorageAdapter } from "media";
|
import { StorageAdapter } from "media/storage/StorageAdapter";
|
||||||
|
|
||||||
class TestAdapter extends StorageAdapter {
|
class TestAdapter extends StorageAdapter {
|
||||||
files: Record<string, FileBody> = {};
|
files: Record<string, FileBody> = {};
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import { afterAll, beforeAll, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
import { afterAll, beforeAll, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
||||||
import { createApp } from "../../src";
|
import { createApp } from "core/test/utils";
|
||||||
import { AuthController } from "../../src/auth/api/AuthController";
|
import { AuthController } from "../../src/auth/api/AuthController";
|
||||||
import { em, entity, make, text } from "../../src/data";
|
import { em, entity, make, text } from "data/prototype";
|
||||||
import { AppAuth, type ModuleBuildContext } from "../../src/modules";
|
import { AppAuth, type ModuleBuildContext } from "modules";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "../helper";
|
import { disableConsoleLog, enableConsoleLog } from "../helper";
|
||||||
// @ts-ignore
|
|
||||||
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
||||||
|
|
||||||
describe("AppAuth", () => {
|
describe("AppAuth", () => {
|
||||||
|
test.only("...", () => {
|
||||||
|
const auth = new AppAuth({});
|
||||||
|
console.log(auth.toJSON());
|
||||||
|
console.log(auth.config);
|
||||||
|
});
|
||||||
|
|
||||||
moduleTestSuite(AppAuth);
|
moduleTestSuite(AppAuth);
|
||||||
|
|
||||||
let ctx: ModuleBuildContext;
|
let ctx: ModuleBuildContext;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { beforeEach, describe, expect, test } from "bun:test";
|
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 { fieldsSchema } from "../../src/data/data-schema";
|
||||||
import { AppData, type ModuleBuildContext } from "../../src/modules";
|
import { AppData, type ModuleBuildContext } from "../../src/modules";
|
||||||
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { createApp, registries } from "../../src";
|
import { createApp } from "core/test/utils";
|
||||||
import { em, entity, text } from "../../src/data";
|
import { em, entity, text } from "data/prototype";
|
||||||
|
import { registries } from "modules/registries";
|
||||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||||
import { AppMedia } from "../../src/modules";
|
import { AppMedia } from "../../src/media/AppMedia";
|
||||||
import { moduleTestSuite } from "./module-test-suite";
|
import { moduleTestSuite } from "./module-test-suite";
|
||||||
|
|
||||||
describe("AppMedia", () => {
|
describe("AppMedia", () => {
|
||||||
|
test.only("...", () => {
|
||||||
|
const media = new AppMedia();
|
||||||
|
console.log(media.toJSON());
|
||||||
|
});
|
||||||
|
|
||||||
moduleTestSuite(AppMedia);
|
moduleTestSuite(AppMedia);
|
||||||
|
|
||||||
test("should allow additional fields", async () => {
|
test("should allow additional fields", async () => {
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { stripMark } from "../../src/core/utils";
|
import { s, stripMark } from "core/utils/schema";
|
||||||
import { type TSchema, Type } from "@sinclair/typebox";
|
import { em, entity, index, text } from "data/prototype";
|
||||||
import { EntityManager, em, entity, index, text } from "../../src/data";
|
import { EntityManager } from "data/entities/EntityManager";
|
||||||
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
||||||
import { Module } from "../../src/modules/Module";
|
import { Module } from "../../src/modules/Module";
|
||||||
|
import { ModuleHelper } from "modules/ModuleHelper";
|
||||||
|
|
||||||
function createModule<Schema extends TSchema>(schema: Schema) {
|
function createModule<Schema extends s.Schema>(schema: Schema) {
|
||||||
class TestModule extends Module<typeof schema> {
|
return class TestModule extends Module<Schema> {
|
||||||
getSchema() {
|
getSchema() {
|
||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
@@ -16,9 +17,7 @@ function createModule<Schema extends TSchema>(schema: Schema) {
|
|||||||
override useForceParse() {
|
override useForceParse() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return TestModule;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Module", async () => {
|
describe("Module", async () => {
|
||||||
@@ -26,7 +25,7 @@ describe("Module", async () => {
|
|||||||
test("listener", async () => {
|
test("listener", async () => {
|
||||||
let result: any;
|
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" });
|
const m = new module({ a: "test" });
|
||||||
|
|
||||||
await m.schema().set({ a: "test2" });
|
await m.schema().set({ a: "test2" });
|
||||||
@@ -42,13 +41,13 @@ describe("Module", async () => {
|
|||||||
describe("db schema", () => {
|
describe("db schema", () => {
|
||||||
class M extends Module {
|
class M extends Module {
|
||||||
override getSchema() {
|
override getSchema() {
|
||||||
return Type.Object({});
|
return s.object({});
|
||||||
}
|
}
|
||||||
|
|
||||||
prt = {
|
prt = {
|
||||||
ensureEntity: this.ensureEntity.bind(this),
|
ensureEntity: this.ctx.helper.ensureEntity.bind(this.ctx.helper),
|
||||||
ensureIndex: this.ensureIndex.bind(this),
|
ensureIndex: this.ctx.helper.ensureIndex.bind(this.ctx.helper),
|
||||||
ensureSchema: this.ensureSchema.bind(this),
|
ensureSchema: this.ctx.helper.ensureSchema.bind(this.ctx.helper),
|
||||||
};
|
};
|
||||||
|
|
||||||
get em() {
|
get em() {
|
||||||
@@ -63,7 +62,11 @@ describe("Module", async () => {
|
|||||||
_em.relations,
|
_em.relations,
|
||||||
_em.indices,
|
_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) {
|
function flat(_em: EntityManager) {
|
||||||
return {
|
return {
|
||||||
@@ -143,14 +146,9 @@ describe("Module", async () => {
|
|||||||
|
|
||||||
// this should only add the field "important"
|
// this should only add the field "important"
|
||||||
m.prt.ensureEntity(
|
m.prt.ensureEntity(
|
||||||
entity(
|
entity("u", {
|
||||||
"u",
|
important: text(),
|
||||||
{
|
}),
|
||||||
important: text(),
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
"system",
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(m.ctx.flags.sync_required).toBe(true);
|
expect(m.ctx.flags.sync_required).toBe(true);
|
||||||
@@ -159,8 +157,7 @@ describe("Module", async () => {
|
|||||||
{
|
{
|
||||||
name: "u",
|
name: "u",
|
||||||
fields: ["id", "name", "important"],
|
fields: ["id", "name", "important"],
|
||||||
// ensured type must be present
|
type: "regular",
|
||||||
type: "system",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "p",
|
name: "p",
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
||||||
import { disableConsoleLog, enableConsoleLog, stripMark } from "core/utils";
|
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||||
import { Type } from "@sinclair/typebox";
|
|
||||||
import { Connection, entity, text } from "data";
|
|
||||||
import { Module } from "modules/Module";
|
import { Module } from "modules/Module";
|
||||||
import { type ConfigTable, getDefaultConfig, ModuleManager } from "modules/ModuleManager";
|
import { type ConfigTable, getDefaultConfig, ModuleManager } from "modules/ModuleManager";
|
||||||
import { CURRENT_VERSION, TABLE_NAME } from "modules/migrations";
|
import { CURRENT_VERSION, TABLE_NAME } from "modules/migrations";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
import { diff } from "core/object/diff";
|
import { s, stripMark } from "core/utils/schema";
|
||||||
import type { Static } from "@sinclair/typebox";
|
import { Connection } from "data/connection/Connection";
|
||||||
|
import { entity, text } from "data/prototype";
|
||||||
|
|
||||||
describe("ModuleManager", async () => {
|
describe("ModuleManager", async () => {
|
||||||
test("s1: no config, no build", async () => {
|
test("s1: no config, no build", async () => {
|
||||||
@@ -92,7 +92,11 @@ describe("ModuleManager", async () => {
|
|||||||
|
|
||||||
await mm2.build();
|
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).toBeDefined();
|
||||||
expect(mm2.configs().data.entities?.test?.fields?.content).toBeDefined();
|
expect(mm2.configs().data.entities?.test?.fields?.content).toBeDefined();
|
||||||
expect(mm2.get("data").toJSON().entities?.test?.fields?.content).toBeDefined();
|
expect(mm2.get("data").toJSON().entities?.test?.fields?.content).toBeDefined();
|
||||||
@@ -257,10 +261,10 @@ describe("ModuleManager", async () => {
|
|||||||
// @todo: add tests for migrations (check "backup" and new version)
|
// @todo: add tests for migrations (check "backup" and new version)
|
||||||
|
|
||||||
describe("revert", async () => {
|
describe("revert", async () => {
|
||||||
const failingModuleSchema = Type.Object({
|
const failingModuleSchema = s.partialObject({
|
||||||
value: Type.Optional(Type.Number()),
|
value: s.number(),
|
||||||
});
|
});
|
||||||
class FailingModule extends Module<typeof failingModuleSchema> {
|
class FailingModule extends Module<s.Static<typeof failingModuleSchema>> {
|
||||||
getSchema() {
|
getSchema() {
|
||||||
return failingModuleSchema;
|
return failingModuleSchema;
|
||||||
}
|
}
|
||||||
@@ -431,11 +435,11 @@ describe("ModuleManager", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("validate & revert", () => {
|
describe("validate & revert", () => {
|
||||||
const schema = Type.Object({
|
const schema = s.object({
|
||||||
value: Type.Array(Type.Number(), { default: [] }),
|
value: s.array(s.number()),
|
||||||
});
|
});
|
||||||
type SampleSchema = Static<typeof schema>;
|
type SampleSchema = s.Static<typeof schema>;
|
||||||
class Sample extends Module<typeof schema> {
|
class Sample extends Module<SampleSchema> {
|
||||||
getSchema() {
|
getSchema() {
|
||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { beforeEach, describe, expect, it } from "bun:test";
|
import { beforeEach, describe, expect, it } from "bun:test";
|
||||||
|
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { Guard } from "../../src/auth";
|
import { Guard } from "auth/authorize/Guard";
|
||||||
import { DebugLogger } from "../../src/core";
|
import { EventManager } from "core/events";
|
||||||
import { EventManager } from "../../src/core/events";
|
import { EntityManager } from "data/entities/EntityManager";
|
||||||
import { Default, stripMark } from "../../src/core/utils";
|
import { Module, type ModuleBuildContext } from "modules/Module";
|
||||||
import { EntityManager } from "../../src/data";
|
|
||||||
import { Module, type ModuleBuildContext } from "../../src/modules/Module";
|
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
|
import { ModuleHelper } from "modules/ModuleHelper";
|
||||||
|
import { DebugLogger, McpServer } from "bknd/utils";
|
||||||
|
|
||||||
export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildContext {
|
export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildContext {
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
return {
|
const ctx = {
|
||||||
connection: dummyConnection,
|
connection: dummyConnection,
|
||||||
server: new Hono(),
|
server: new Hono(),
|
||||||
em: new EntityManager([], dummyConnection),
|
em: new EntityManager([], dummyConnection),
|
||||||
@@ -19,8 +19,13 @@ export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildCon
|
|||||||
guard: new Guard(),
|
guard: new Guard(),
|
||||||
flags: Module.ctx_flags,
|
flags: Module.ctx_flags,
|
||||||
logger: new DebugLogger(false),
|
logger: new DebugLogger(false),
|
||||||
|
mcp: new McpServer(),
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
|
return {
|
||||||
|
...ctx,
|
||||||
|
helper: new ModuleHelper(ctx as any),
|
||||||
|
} as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function moduleTestSuite(module: { new (): Module }) {
|
export function moduleTestSuite(module: { new (): Module }) {
|
||||||
@@ -40,7 +45,8 @@ export function moduleTestSuite(module: { new (): Module }) {
|
|||||||
it("uses the default config", async () => {
|
it("uses the default config", async () => {
|
||||||
const m = new module();
|
const m = new module();
|
||||||
await m.setContext(ctx).build();
|
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][];
|
] satisfies [string, Exclude<JSONSchema, boolean>, boolean][];
|
||||||
|
|
||||||
for (const [pointer, schema, output] of examples) {
|
for (const [pointer, schema, output] of examples) {
|
||||||
expect(utils.isRequired(new Draft2019(schema), pointer, schema)).toBe(output);
|
expect(utils.isRequired(new Draft2019(schema), pointer, schema), `${pointer} `).toBe(
|
||||||
|
output,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+20
-1
@@ -1,6 +1,24 @@
|
|||||||
import pkg from "./package.json" with { type: "json" };
|
import pkg from "./package.json" with { type: "json" };
|
||||||
import c from "picocolors";
|
import c from "picocolors";
|
||||||
import { formatNumber } from "core/utils";
|
import { formatNumber } from "bknd/utils";
|
||||||
|
import * as esbuild from "esbuild";
|
||||||
|
|
||||||
|
if (process.env.DEBUG) {
|
||||||
|
await esbuild.build({
|
||||||
|
entryPoints: ["./src/cli/index.ts"],
|
||||||
|
outdir: "./dist/cli",
|
||||||
|
platform: "node",
|
||||||
|
minify: false,
|
||||||
|
format: "esm",
|
||||||
|
bundle: true,
|
||||||
|
external: ["jsonv-ts", "jsonv-ts/*"],
|
||||||
|
define: {
|
||||||
|
__isDev: "0",
|
||||||
|
__version: JSON.stringify(pkg.version),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
const result = await Bun.build({
|
const result = await Bun.build({
|
||||||
entrypoints: ["./src/cli/index.ts"],
|
entrypoints: ["./src/cli/index.ts"],
|
||||||
@@ -8,6 +26,7 @@ const result = await Bun.build({
|
|||||||
outdir: "./dist/cli",
|
outdir: "./dist/cli",
|
||||||
env: "PUBLIC_*",
|
env: "PUBLIC_*",
|
||||||
minify: true,
|
minify: true,
|
||||||
|
external: ["jsonv-ts", "jsonv-ts/*"],
|
||||||
define: {
|
define: {
|
||||||
__isDev: "0",
|
__isDev: "0",
|
||||||
__version: JSON.stringify(pkg.version),
|
__version: JSON.stringify(pkg.version),
|
||||||
|
|||||||
+99
-59
@@ -1,6 +1,7 @@
|
|||||||
import { $ } from "bun";
|
import { $ } from "bun";
|
||||||
import * as tsup from "tsup";
|
import * as tsup from "tsup";
|
||||||
import pkg from "./package.json" with { type: "json" };
|
import pkg from "./package.json" with { type: "json" };
|
||||||
|
import c from "picocolors";
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
const watch = args.includes("--watch");
|
const watch = args.includes("--watch");
|
||||||
@@ -9,6 +10,14 @@ const types = args.includes("--types");
|
|||||||
const sourcemap = args.includes("--sourcemap");
|
const sourcemap = args.includes("--sourcemap");
|
||||||
const clean = args.includes("--clean");
|
const clean = args.includes("--clean");
|
||||||
|
|
||||||
|
// silence tsup
|
||||||
|
const oldConsole = {
|
||||||
|
log: console.log,
|
||||||
|
warn: console.warn,
|
||||||
|
};
|
||||||
|
console.log = () => {};
|
||||||
|
console.warn = () => {};
|
||||||
|
|
||||||
const define = {
|
const define = {
|
||||||
__isDev: "0",
|
__isDev: "0",
|
||||||
__version: JSON.stringify(pkg.version),
|
__version: JSON.stringify(pkg.version),
|
||||||
@@ -27,11 +36,11 @@ function buildTypes() {
|
|||||||
Bun.spawn(["bun", "build:types"], {
|
Bun.spawn(["bun", "build:types"], {
|
||||||
stdout: "inherit",
|
stdout: "inherit",
|
||||||
onExit: () => {
|
onExit: () => {
|
||||||
console.info("Types built");
|
oldConsole.log(c.cyan("[Types]"), c.green("built"));
|
||||||
Bun.spawn(["bun", "tsc-alias"], {
|
Bun.spawn(["bun", "tsc-alias"], {
|
||||||
stdout: "inherit",
|
stdout: "inherit",
|
||||||
onExit: () => {
|
onExit: () => {
|
||||||
console.info("Types aliased");
|
oldConsole.log(c.cyan("[Types]"), c.green("aliased"));
|
||||||
types_running = false;
|
types_running = false;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -39,6 +48,10 @@ function buildTypes() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (types && !watch) {
|
||||||
|
buildTypes();
|
||||||
|
}
|
||||||
|
|
||||||
let watcher_timeout: any;
|
let watcher_timeout: any;
|
||||||
function delayTypes() {
|
function delayTypes() {
|
||||||
if (!watch || !types) return;
|
if (!watch || !types) return;
|
||||||
@@ -48,37 +61,28 @@ function delayTypes() {
|
|||||||
watcher_timeout = setTimeout(buildTypes, 1000);
|
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
// collection of always-external packages
|
// collection of always-external packages
|
||||||
const external = ["bun:test", "node:test", "node:assert/strict", "@libsql/client"] as const;
|
const external = [
|
||||||
|
"bun:test",
|
||||||
|
"node:test",
|
||||||
|
"node:assert/strict",
|
||||||
|
"@libsql/client",
|
||||||
|
"bknd",
|
||||||
|
/^bknd\/.*/,
|
||||||
|
"jsonv-ts",
|
||||||
|
/^jsonv-ts\/.*/,
|
||||||
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Building backend and general API
|
* Building backend and general API
|
||||||
*/
|
*/
|
||||||
async function buildApi() {
|
async function buildApi() {
|
||||||
banner("Building API");
|
|
||||||
await tsup.build({
|
await tsup.build({
|
||||||
minify,
|
minify,
|
||||||
sourcemap,
|
sourcemap,
|
||||||
watch,
|
watch,
|
||||||
define,
|
define,
|
||||||
entry: [
|
entry: ["src/index.ts", "src/core/utils/index.ts", "src/plugins/index.ts"],
|
||||||
"src/index.ts",
|
|
||||||
"src/core/index.ts",
|
|
||||||
"src/core/utils/index.ts",
|
|
||||||
"src/data/index.ts",
|
|
||||||
"src/media/index.ts",
|
|
||||||
],
|
|
||||||
outDir: "dist",
|
outDir: "dist",
|
||||||
external: [...external],
|
external: [...external],
|
||||||
metafile: true,
|
metafile: true,
|
||||||
@@ -91,6 +95,7 @@ async function buildApi() {
|
|||||||
},
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
delayTypes();
|
delayTypes();
|
||||||
|
oldConsole.log(c.cyan("[API]"), c.green("built"));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -134,7 +139,6 @@ async function buildUi() {
|
|||||||
},
|
},
|
||||||
} satisfies tsup.Options;
|
} satisfies tsup.Options;
|
||||||
|
|
||||||
banner("Building UI");
|
|
||||||
await tsup.build({
|
await tsup.build({
|
||||||
...base,
|
...base,
|
||||||
entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"],
|
entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"],
|
||||||
@@ -142,10 +146,10 @@ async function buildUi() {
|
|||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await rewriteClient("./dist/ui/index.js");
|
await rewriteClient("./dist/ui/index.js");
|
||||||
delayTypes();
|
delayTypes();
|
||||||
|
oldConsole.log(c.cyan("[UI]"), c.green("built"));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
banner("Building Client");
|
|
||||||
await tsup.build({
|
await tsup.build({
|
||||||
...base,
|
...base,
|
||||||
entry: ["src/ui/client/index.ts"],
|
entry: ["src/ui/client/index.ts"],
|
||||||
@@ -153,6 +157,7 @@ async function buildUi() {
|
|||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await rewriteClient("./dist/ui/client/index.js");
|
await rewriteClient("./dist/ui/client/index.js");
|
||||||
delayTypes();
|
delayTypes();
|
||||||
|
oldConsole.log(c.cyan("[UI]"), "Client", c.green("built"));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -163,7 +168,6 @@ async function buildUi() {
|
|||||||
* - ui/client is external, and after built replaced with "bknd/client"
|
* - ui/client is external, and after built replaced with "bknd/client"
|
||||||
*/
|
*/
|
||||||
async function buildUiElements() {
|
async function buildUiElements() {
|
||||||
banner("Building UI Elements");
|
|
||||||
await tsup.build({
|
await tsup.build({
|
||||||
minify,
|
minify,
|
||||||
sourcemap,
|
sourcemap,
|
||||||
@@ -197,6 +201,7 @@ async function buildUiElements() {
|
|||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await rewriteClient("./dist/ui/elements/index.js");
|
await rewriteClient("./dist/ui/elements/index.js");
|
||||||
delayTypes();
|
delayTypes();
|
||||||
|
oldConsole.log(c.cyan("[UI]"), "Elements", c.green("built"));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -217,6 +222,7 @@ function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsu
|
|||||||
splitting: false,
|
splitting: false,
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
delayTypes();
|
delayTypes();
|
||||||
|
oldConsole.log(c.cyan("[Adapter]"), adapter || "base", c.green("built"));
|
||||||
},
|
},
|
||||||
...overrides,
|
...overrides,
|
||||||
define: {
|
define: {
|
||||||
@@ -225,51 +231,85 @@ function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsu
|
|||||||
},
|
},
|
||||||
external: [
|
external: [
|
||||||
/^cloudflare*/,
|
/^cloudflare*/,
|
||||||
/^@?(hono|libsql).*?/,
|
/^@?hono.*?/,
|
||||||
/^(bknd|react|next|node).*?/,
|
/^(bknd|react|next|node).*?/,
|
||||||
/.*\.(html)$/,
|
/.*\.(html)$/,
|
||||||
|
...external,
|
||||||
...(Array.isArray(overrides.external) ? overrides.external : []),
|
...(Array.isArray(overrides.external) ? overrides.external : []),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildAdapters() {
|
async function buildAdapters() {
|
||||||
banner("Building Adapters");
|
await Promise.all([
|
||||||
// base adapter handles
|
// base adapter handles
|
||||||
await tsup.build({
|
tsup.build({
|
||||||
...baseConfig(""),
|
...baseConfig(""),
|
||||||
entry: ["src/adapter/index.ts"],
|
entry: ["src/adapter/index.ts"],
|
||||||
outDir: "dist/adapter",
|
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 tsup.build({
|
// specific adatpers
|
||||||
...baseConfig("vite"),
|
tsup.build(baseConfig("react-router")),
|
||||||
platform: "node",
|
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({
|
tsup.build({
|
||||||
...baseConfig("nextjs"),
|
...baseConfig("vite"),
|
||||||
platform: "node",
|
platform: "node",
|
||||||
});
|
}),
|
||||||
|
|
||||||
await tsup.build({
|
tsup.build({
|
||||||
...baseConfig("node"),
|
...baseConfig("nextjs"),
|
||||||
platform: "node",
|
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 Promise.all([buildApi(), buildUi(), buildUiElements(), buildAdapters()]);
|
||||||
await buildUi();
|
|
||||||
await buildUiElements();
|
|
||||||
await buildAdapters();
|
|
||||||
|
|||||||
+2
-1
@@ -2,4 +2,5 @@
|
|||||||
#registry = "http://localhost:4873"
|
#registry = "http://localhost:4873"
|
||||||
|
|
||||||
[test]
|
[test]
|
||||||
coverageSkipTestFiles = true
|
coverageSkipTestFiles = true
|
||||||
|
console.depth = 10
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { createApp } from "bknd/adapter/bun";
|
||||||
|
|
||||||
|
async function generate() {
|
||||||
|
console.info("Generating MCP documentation...");
|
||||||
|
const app = await createApp({
|
||||||
|
initialConfig: {
|
||||||
|
server: {
|
||||||
|
mcp: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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();
|
||||||
+60
-30
@@ -3,7 +3,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"bin": "./dist/cli/index.js",
|
"bin": "./dist/cli/index.js",
|
||||||
"version": "0.14.0",
|
"version": "0.16.1",
|
||||||
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
|
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
|
||||||
"homepage": "https://bknd.io",
|
"homepage": "https://bknd.io",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -13,6 +13,10 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/bknd-io/bknd/issues"
|
"url": "https://github.com/bknd-io/bknd/issues"
|
||||||
},
|
},
|
||||||
|
"packageManager": "bun@1.2.19",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "BKND_CLI_LOG_LEVEL=debug vite",
|
"dev": "BKND_CLI_LOG_LEVEL=debug vite",
|
||||||
"build": "NODE_ENV=production bun run build.ts --minify --types",
|
"build": "NODE_ENV=production bun run build.ts --minify --types",
|
||||||
@@ -31,17 +35,16 @@
|
|||||||
"test": "ALL_TESTS=1 bun test --bail",
|
"test": "ALL_TESTS=1 bun test --bail",
|
||||||
"test:all": "bun run test && bun run test:node",
|
"test:all": "bun run test && bun run test:node",
|
||||||
"test:bun": "ALL_TESTS=1 bun test --bail",
|
"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:adapters": "bun test src/adapter/**/*.adapter.spec.ts --bail",
|
||||||
"test:coverage": "ALL_TESTS=1 bun test --bail --coverage",
|
"test:coverage": "ALL_TESTS=1 bun test --bail --coverage",
|
||||||
"test:vitest": "vitest run",
|
|
||||||
"test:vitest:watch": "vitest",
|
|
||||||
"test:vitest:coverage": "vitest run --coverage",
|
"test:vitest:coverage": "vitest run --coverage",
|
||||||
"test:e2e": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
"test:e2e:adapters": "bun run e2e/adapters.ts",
|
"test:e2e:adapters": "bun run e2e/adapters.ts",
|
||||||
"test:e2e:ui": "playwright test --ui",
|
"test:e2e:ui": "playwright test --ui",
|
||||||
"test:e2e:debug": "playwright test --debug",
|
"test:e2e:debug": "playwright test --debug",
|
||||||
"test:e2e:report": "playwright show-report"
|
"test:e2e:report": "playwright show-report",
|
||||||
|
"docs:build-assets": "bun internal/docs.build-assets.ts"
|
||||||
},
|
},
|
||||||
"license": "FSL-1.1-MIT",
|
"license": "FSL-1.1-MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -50,10 +53,8 @@
|
|||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@hello-pangea/dnd": "^18.0.1",
|
"@hello-pangea/dnd": "^18.0.1",
|
||||||
"@hono/swagger-ui": "^0.5.1",
|
"@hono/swagger-ui": "^0.5.1",
|
||||||
"@libsql/client": "^0.15.2",
|
|
||||||
"@mantine/core": "^7.17.1",
|
"@mantine/core": "^7.17.1",
|
||||||
"@mantine/hooks": "^7.17.1",
|
"@mantine/hooks": "^7.17.1",
|
||||||
"@sinclair/typebox": "0.34.30",
|
|
||||||
"@tanstack/react-form": "^1.0.5",
|
"@tanstack/react-form": "^1.0.5",
|
||||||
"@uiw/react-codemirror": "^4.23.10",
|
"@uiw/react-codemirror": "^4.23.10",
|
||||||
"@xyflow/react": "^12.4.4",
|
"@xyflow/react": "^12.4.4",
|
||||||
@@ -61,11 +62,11 @@
|
|||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"fast-xml-parser": "^5.0.8",
|
"fast-xml-parser": "^5.0.8",
|
||||||
"json-schema-form-react": "^0.0.2",
|
"hono": "4.8.3",
|
||||||
"json-schema-library": "10.0.0-rc7",
|
"json-schema-library": "10.0.0-rc7",
|
||||||
"json-schema-to-ts": "^3.1.1",
|
"json-schema-to-ts": "^3.1.1",
|
||||||
"kysely": "^0.27.6",
|
"jsonv-ts": "^0.8.1",
|
||||||
"hono": "^4.7.11",
|
"kysely": "0.27.6",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"oauth4webapi": "^2.11.1",
|
"oauth4webapi": "^2.11.1",
|
||||||
"object-path-immutable": "^4.1.2",
|
"object-path-immutable": "^4.1.2",
|
||||||
@@ -75,15 +76,18 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.758.0",
|
"@aws-sdk/client-s3": "^3.758.0",
|
||||||
"@bluwy/giget-core": "^0.1.2",
|
"@bluwy/giget-core": "^0.1.2",
|
||||||
|
"@clack/prompts": "^0.11.0",
|
||||||
|
"@cloudflare/vitest-pool-workers": "^0.8.38",
|
||||||
|
"@cloudflare/workers-types": "^4.20250606.0",
|
||||||
"@dagrejs/dagre": "^1.1.4",
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
"@hono/typebox-validator": "^0.3.3",
|
|
||||||
"@hono/vite-dev-server": "^0.19.1",
|
"@hono/vite-dev-server": "^0.19.1",
|
||||||
"@hookform/resolvers": "^4.1.3",
|
"@hookform/resolvers": "^4.1.3",
|
||||||
"@libsql/kysely-libsql": "^0.4.1",
|
"@libsql/client": "^0.15.9",
|
||||||
"@mantine/modals": "^7.17.1",
|
"@mantine/modals": "^7.17.1",
|
||||||
"@mantine/notifications": "^7.17.1",
|
"@mantine/notifications": "^7.17.1",
|
||||||
"@playwright/test": "^1.51.1",
|
"@playwright/test": "^1.51.1",
|
||||||
"@rjsf/core": "5.22.2",
|
"@rjsf/core": "5.22.2",
|
||||||
|
"@standard-schema/spec": "^1.0.0",
|
||||||
"@tabler/icons-react": "3.18.0",
|
"@tabler/icons-react": "3.18.0",
|
||||||
"@tailwindcss/postcss": "^4.0.12",
|
"@tailwindcss/postcss": "^4.0.12",
|
||||||
"@tailwindcss/vite": "^4.0.12",
|
"@tailwindcss/vite": "^4.0.12",
|
||||||
@@ -99,8 +103,9 @@
|
|||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"jotai": "^2.12.2",
|
"jotai": "^2.12.2",
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.0.0",
|
||||||
"jsonv-ts": "^0.1.0",
|
|
||||||
"kysely-d1": "^0.3.0",
|
"kysely-d1": "^0.3.0",
|
||||||
|
"kysely-generic-sqlite": "^1.2.1",
|
||||||
|
"libsql-stateless-easy": "^1.8.0",
|
||||||
"open": "^10.1.0",
|
"open": "^10.1.0",
|
||||||
"openapi-types": "^12.1.3",
|
"openapi-types": "^12.1.3",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -122,10 +127,10 @@
|
|||||||
"tsx": "^4.19.3",
|
"tsx": "^4.19.3",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"vite": "^6.3.5",
|
"vite": "^6.3.5",
|
||||||
|
"vite-plugin-circular-dependency": "^0.5.0",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
"vitest": "^3.0.9",
|
"vitest": "^3.0.9",
|
||||||
"wouter": "^3.6.0",
|
"wouter": "^3.6.0"
|
||||||
"@cloudflare/workers-types": "^4.20250606.0"
|
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@hono/node-server": "^1.14.3"
|
"@hono/node-server": "^1.14.3"
|
||||||
@@ -158,16 +163,6 @@
|
|||||||
"import": "./dist/ui/client/index.js",
|
"import": "./dist/ui/client/index.js",
|
||||||
"require": "./dist/ui/client/index.js"
|
"require": "./dist/ui/client/index.js"
|
||||||
},
|
},
|
||||||
"./data": {
|
|
||||||
"types": "./dist/types/data/index.d.ts",
|
|
||||||
"import": "./dist/data/index.js",
|
|
||||||
"require": "./dist/data/index.js"
|
|
||||||
},
|
|
||||||
"./core": {
|
|
||||||
"types": "./dist/types/core/index.d.ts",
|
|
||||||
"import": "./dist/core/index.js",
|
|
||||||
"require": "./dist/core/index.js"
|
|
||||||
},
|
|
||||||
"./utils": {
|
"./utils": {
|
||||||
"types": "./dist/types/core/utils/index.d.ts",
|
"types": "./dist/types/core/utils/index.d.ts",
|
||||||
"import": "./dist/core/utils/index.js",
|
"import": "./dist/core/utils/index.js",
|
||||||
@@ -178,16 +173,35 @@
|
|||||||
"import": "./dist/cli/index.js",
|
"import": "./dist/cli/index.js",
|
||||||
"require": "./dist/cli/index.js"
|
"require": "./dist/cli/index.js"
|
||||||
},
|
},
|
||||||
"./media": {
|
"./plugins": {
|
||||||
"types": "./dist/types/media/index.d.ts",
|
"types": "./dist/types/plugins/index.d.ts",
|
||||||
"import": "./dist/media/index.js",
|
"import": "./dist/plugins/index.js",
|
||||||
"require": "./dist/media/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": {
|
"./adapter/cloudflare": {
|
||||||
"types": "./dist/types/adapter/cloudflare/index.d.ts",
|
"types": "./dist/types/adapter/cloudflare/index.d.ts",
|
||||||
"import": "./dist/adapter/cloudflare/index.js",
|
"import": "./dist/adapter/cloudflare/index.js",
|
||||||
"require": "./dist/adapter/cloudflare/index.js"
|
"require": "./dist/adapter/cloudflare/index.js"
|
||||||
},
|
},
|
||||||
|
"./adapter/cloudflare/proxy": {
|
||||||
|
"types": "./dist/types/adapter/cloudflare/proxy.d.ts",
|
||||||
|
"import": "./dist/adapter/cloudflare/proxy.js",
|
||||||
|
"require": "./dist/adapter/cloudflare/proxy.js"
|
||||||
|
},
|
||||||
"./adapter": {
|
"./adapter": {
|
||||||
"types": "./dist/types/adapter/index.d.ts",
|
"types": "./dist/types/adapter/index.d.ts",
|
||||||
"import": "./dist/adapter/index.js"
|
"import": "./dist/adapter/index.js"
|
||||||
@@ -229,7 +243,23 @@
|
|||||||
},
|
},
|
||||||
"./dist/main.css": "./dist/ui/main.css",
|
"./dist/main.css": "./dist/ui/main.css",
|
||||||
"./dist/styles.css": "./dist/ui/styles.css",
|
"./dist/styles.css": "./dist/ui/styles.css",
|
||||||
"./dist/manifest.json": "./dist/static/.vite/manifest.json"
|
"./dist/manifest.json": "./dist/static/.vite/manifest.json",
|
||||||
|
"./static/*": "./dist/static/*"
|
||||||
|
},
|
||||||
|
"typesVersions": {
|
||||||
|
"*": {
|
||||||
|
"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": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
|||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
import type { SafeUser } from "auth";
|
import type { SafeUser } from "bknd";
|
||||||
import { AuthApi, type AuthApiOptions } from "auth/api/AuthApi";
|
import { AuthApi, type AuthApiOptions } from "auth/api/AuthApi";
|
||||||
import { DataApi, type DataApiOptions } from "data/api/DataApi";
|
import { DataApi, type DataApiOptions } from "data/api/DataApi";
|
||||||
import { decode } from "hono/jwt";
|
import { decode } from "hono/jwt";
|
||||||
import { MediaApi, type MediaApiOptions } from "media/api/MediaApi";
|
import { MediaApi, type MediaApiOptions } from "media/api/MediaApi";
|
||||||
import { SystemApi } from "modules/SystemApi";
|
import { SystemApi } from "modules/SystemApi";
|
||||||
import { omitKeys } from "core/utils";
|
import { omitKeys } from "bknd/utils";
|
||||||
import type { BaseModuleApiOptions } from "modules";
|
import type { BaseModuleApiOptions } from "modules";
|
||||||
|
|
||||||
export type TApiUser = SafeUser;
|
export type TApiUser = SafeUser;
|
||||||
|
|||||||
+140
-52
@@ -1,7 +1,8 @@
|
|||||||
import type { CreateUserPayload } from "auth/AppAuth";
|
import type { CreateUserPayload } from "auth/AppAuth";
|
||||||
import { $console } from "core";
|
import { $console, McpClient } from "bknd/utils";
|
||||||
import { Event } from "core/events";
|
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 type { Hono } from "hono";
|
||||||
import {
|
import {
|
||||||
ModuleManager,
|
ModuleManager,
|
||||||
@@ -14,17 +15,55 @@ import {
|
|||||||
import * as SystemPermissions from "modules/permissions";
|
import * as SystemPermissions from "modules/permissions";
|
||||||
import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
|
import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
|
||||||
import { SystemController } from "modules/server/SystemController";
|
import { SystemController } from "modules/server/SystemController";
|
||||||
|
import type { MaybePromise } from "core/types";
|
||||||
|
import type { ServerEnv } from "modules/Controller";
|
||||||
|
import type { IEmailDriver, ICacheDriver } from "core/drivers";
|
||||||
|
|
||||||
// biome-ignore format: must be here
|
// biome-ignore format: must be here
|
||||||
import { Api, type ApiOptions } from "Api";
|
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> {}
|
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";
|
static override slug = "app-config-updated";
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @type {Event<{ app: App }>}
|
||||||
|
*/
|
||||||
export class AppBuiltEvent extends AppEvent {
|
export class AppBuiltEvent extends AppEvent {
|
||||||
static override slug = "app-built";
|
static override slug = "app-built";
|
||||||
}
|
}
|
||||||
@@ -50,16 +89,16 @@ export type AppOptions = {
|
|||||||
seed?: (ctx: ModuleBuildContext & { app: App }) => Promise<void>;
|
seed?: (ctx: ModuleBuildContext & { app: App }) => Promise<void>;
|
||||||
manager?: Omit<ModuleManagerOptions, "initial" | "onUpdated" | "seed">;
|
manager?: Omit<ModuleManagerOptions, "initial" | "onUpdated" | "seed">;
|
||||||
asyncEventsMode?: "sync" | "async" | "none";
|
asyncEventsMode?: "sync" | "async" | "none";
|
||||||
|
drivers?: {
|
||||||
|
email?: IEmailDriver;
|
||||||
|
cache?: ICacheDriver;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
export type CreateAppConfig = {
|
export type CreateAppConfig = {
|
||||||
connection?:
|
/**
|
||||||
| Connection
|
* bla
|
||||||
| {
|
*/
|
||||||
// @deprecated
|
connection?: Connection | { url: string };
|
||||||
type: "libsql";
|
|
||||||
config: LibSqlCredentials;
|
|
||||||
}
|
|
||||||
| LibSqlCredentials;
|
|
||||||
initialConfig?: InitialModuleConfigs;
|
initialConfig?: InitialModuleConfigs;
|
||||||
options?: AppOptions;
|
options?: AppOptions;
|
||||||
};
|
};
|
||||||
@@ -67,29 +106,41 @@ export type CreateAppConfig = {
|
|||||||
export type AppConfig = InitialModuleConfigs;
|
export type AppConfig = InitialModuleConfigs;
|
||||||
export type LocalApiOptions = Request | ApiOptions;
|
export type LocalApiOptions = Request | ApiOptions;
|
||||||
|
|
||||||
export class App {
|
export class App<C extends Connection = Connection, Options extends AppOptions = AppOptions> {
|
||||||
static readonly Events = AppEvents;
|
static readonly Events = AppEvents;
|
||||||
|
|
||||||
modules: ModuleManager;
|
modules: ModuleManager;
|
||||||
adminController?: AdminController;
|
adminController?: AdminController;
|
||||||
_id: string = crypto.randomUUID();
|
_id: string = crypto.randomUUID();
|
||||||
|
plugins: Map<string, AppPluginConfig> = new Map();
|
||||||
|
drivers: Options["drivers"] = {};
|
||||||
|
|
||||||
private trigger_first_boot = false;
|
private trigger_first_boot = false;
|
||||||
private plugins: AppPlugin[];
|
|
||||||
private _building: boolean = false;
|
private _building: boolean = false;
|
||||||
|
private _systemController: SystemController | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private connection: Connection,
|
public connection: C,
|
||||||
_initialConfig?: InitialModuleConfigs,
|
_initialConfig?: InitialModuleConfigs,
|
||||||
private options?: AppOptions,
|
private options?: Options,
|
||||||
) {
|
) {
|
||||||
this.plugins = options?.plugins ?? [];
|
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");
|
||||||
this.modules = new ModuleManager(connection, {
|
this.modules = new ModuleManager(connection, {
|
||||||
...(options?.manager ?? {}),
|
...(options?.manager ?? {}),
|
||||||
initial: _initialConfig,
|
initial: _initialConfig,
|
||||||
onUpdated: this.onUpdated.bind(this),
|
onUpdated: this.onUpdated.bind(this),
|
||||||
onFirstBoot: this.onFirstBoot.bind(this),
|
onFirstBoot: this.onFirstBoot.bind(this),
|
||||||
onServerInit: this.onServerInit.bind(this),
|
onServerInit: this.onServerInit.bind(this),
|
||||||
|
onModulesBuilt: this.onModulesBuilt.bind(this),
|
||||||
});
|
});
|
||||||
this.modules.ctx().emgr.registerEvents(AppEvents);
|
this.modules.ctx().emgr.registerEvents(AppEvents);
|
||||||
}
|
}
|
||||||
@@ -98,6 +149,32 @@ export class App {
|
|||||||
return this.modules.ctx().emgr;
|
return this.modules.ctx().emgr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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; fetch?: boolean; forceBuild?: boolean }) {
|
async build(options?: { sync?: boolean; fetch?: boolean; forceBuild?: boolean }) {
|
||||||
// prevent multiple concurrent builds
|
// prevent multiple concurrent builds
|
||||||
if (this._building) {
|
if (this._building) {
|
||||||
@@ -106,24 +183,24 @@ export class App {
|
|||||||
}
|
}
|
||||||
if (!options?.forceBuild) return;
|
if (!options?.forceBuild) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.runPlugins("beforeBuild");
|
||||||
this._building = true;
|
this._building = true;
|
||||||
|
|
||||||
if (options?.sync) this.modules.ctx().flags.sync_required = true;
|
if (options?.sync) this.modules.ctx().flags.sync_required = true;
|
||||||
await this.modules.build({ fetch: options?.fetch });
|
await this.modules.build({ fetch: options?.fetch });
|
||||||
|
|
||||||
const { guard, server } = this.modules.ctx();
|
const { guard } = this.modules.ctx();
|
||||||
|
|
||||||
// load system controller
|
// load system controller
|
||||||
guard.registerPermissions(Object.values(SystemPermissions));
|
guard.registerPermissions(Object.values(SystemPermissions));
|
||||||
server.route("/api/system", new SystemController(this).getController());
|
this._systemController = new SystemController(this);
|
||||||
|
this._systemController.register(this);
|
||||||
// load plugins
|
|
||||||
if (this.plugins.length > 0) {
|
|
||||||
await Promise.all(this.plugins.map((plugin) => plugin(this)));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// emit built event
|
||||||
$console.log("App built");
|
$console.log("App built");
|
||||||
await this.emgr.emit(new AppBuiltEvent({ app: this }));
|
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
|
// first boot is set from ModuleManager when there wasn't a config table
|
||||||
if (this.trigger_first_boot) {
|
if (this.trigger_first_boot) {
|
||||||
@@ -150,6 +227,10 @@ export class App {
|
|||||||
return this.modules.ctx().em;
|
return this.modules.ctx().em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get mcp() {
|
||||||
|
return this._systemController?._mcpServer;
|
||||||
|
}
|
||||||
|
|
||||||
get fetch(): Hono["fetch"] {
|
get fetch(): Hono["fetch"] {
|
||||||
return this.server.fetch as any;
|
return this.server.fetch as any;
|
||||||
}
|
}
|
||||||
@@ -208,6 +289,17 @@ export class App {
|
|||||||
return new Api({ host: "http://localhost", ...(options ?? {}), fetcher });
|
return new Api({ host: "http://localhost", ...(options ?? {}), fetcher });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMcpClient() {
|
||||||
|
if (!this.mcp) {
|
||||||
|
throw new Error("MCP is not enabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new McpClient({
|
||||||
|
url: "http://localhost/mcp",
|
||||||
|
fetch: this.server.request,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async onUpdated<Module extends keyof Modules>(module: Module, config: ModuleConfigs[Module]) {
|
async onUpdated<Module extends keyof Modules>(module: Module, config: ModuleConfigs[Module]) {
|
||||||
// if the EventManager was disabled, we assume we shouldn't
|
// if the EventManager was disabled, we assume we shouldn't
|
||||||
// respond to events, such as "onUpdated".
|
// respond to events, such as "onUpdated".
|
||||||
@@ -220,15 +312,16 @@ export class App {
|
|||||||
$console.log("App config updated", module);
|
$console.log("App config updated", module);
|
||||||
// @todo: potentially double syncing
|
// @todo: potentially double syncing
|
||||||
await this.build({ sync: true });
|
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");
|
$console.log("App first boot");
|
||||||
this.trigger_first_boot = true;
|
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) => {
|
server.use(async (c, next) => {
|
||||||
c.set("app", this);
|
c.set("app", this);
|
||||||
await this.emgr.emit(new AppRequest({ app: this, request: c.req.raw }));
|
await this.emgr.emit(new AppRequest({ app: this, request: c.req.raw }));
|
||||||
@@ -258,35 +351,30 @@ export class App {
|
|||||||
if (this.options?.manager?.onServerInit) {
|
if (this.options?.manager?.onServerInit) {
|
||||||
this.options.manager.onServerInit(server);
|
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 = {}) {
|
export function createApp(config: CreateAppConfig = {}) {
|
||||||
let connection: Connection | undefined = undefined;
|
if (!config.connection || !Connection.isConnection(config.connection)) {
|
||||||
|
|
||||||
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) {
|
|
||||||
throw new Error("Invalid connection");
|
throw new Error("Invalid connection");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new App(connection, config.initialConfig, config.options);
|
return new App(config.connection, config.initialConfig, config.options);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { type RuntimeBkndConfig, createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
|
import { type RuntimeBkndConfig, createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
|
||||||
import { registerLocalMediaAdapter } from "bknd/adapter/node";
|
import { registerLocalMediaAdapter } from ".";
|
||||||
import { config } from "bknd/core";
|
import { config, type App } from "bknd";
|
||||||
import type { ServeOptions } from "bun";
|
import type { ServeOptions } from "bun";
|
||||||
import { serveStatic } from "hono/bun";
|
import { serveStatic } from "hono/bun";
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ type BunEnv = Bun.Env;
|
|||||||
export type BunBkndConfig<Env = BunEnv> = RuntimeBkndConfig<Env> & Omit<ServeOptions, "fetch">;
|
export type BunBkndConfig<Env = BunEnv> = RuntimeBkndConfig<Env> & Omit<ServeOptions, "fetch">;
|
||||||
|
|
||||||
export async function createApp<Env = BunEnv>(
|
export async function createApp<Env = BunEnv>(
|
||||||
{ distPath, ...config }: BunBkndConfig<Env> = {},
|
{ distPath, serveStatic: _serveStatic, ...config }: BunBkndConfig<Env> = {},
|
||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
opts?: RuntimeOptions,
|
opts?: RuntimeOptions,
|
||||||
) {
|
) {
|
||||||
@@ -20,8 +20,12 @@ export async function createApp<Env = BunEnv>(
|
|||||||
|
|
||||||
return await createRuntimeApp(
|
return await createRuntimeApp(
|
||||||
{
|
{
|
||||||
|
serveStatic:
|
||||||
|
_serveStatic ??
|
||||||
|
serveStatic({
|
||||||
|
root,
|
||||||
|
}),
|
||||||
...config,
|
...config,
|
||||||
serveStatic: serveStatic({ root }),
|
|
||||||
},
|
},
|
||||||
args ?? (process.env as Env),
|
args ?? (process.env as Env),
|
||||||
opts,
|
opts,
|
||||||
@@ -33,8 +37,11 @@ export function createHandler<Env = BunEnv>(
|
|||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
opts?: RuntimeOptions,
|
opts?: RuntimeOptions,
|
||||||
) {
|
) {
|
||||||
|
let app: App | undefined;
|
||||||
return async (req: Request) => {
|
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), opts);
|
||||||
|
}
|
||||||
return app.fetch(req);
|
return app.fetch(req);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -49,6 +56,7 @@ export function serve<Env = BunEnv>(
|
|||||||
onBuilt,
|
onBuilt,
|
||||||
buildConfig,
|
buildConfig,
|
||||||
adminOptions,
|
adminOptions,
|
||||||
|
serveStatic,
|
||||||
...serveOptions
|
...serveOptions
|
||||||
}: BunBkndConfig<Env> = {},
|
}: BunBkndConfig<Env> = {},
|
||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
@@ -66,11 +74,12 @@ export function serve<Env = BunEnv>(
|
|||||||
buildConfig,
|
buildConfig,
|
||||||
adminOptions,
|
adminOptions,
|
||||||
distPath,
|
distPath,
|
||||||
|
serveStatic,
|
||||||
},
|
},
|
||||||
args,
|
args,
|
||||||
opts,
|
opts,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Server is running on http://localhost:${port}`);
|
console.info(`Server is running on http://localhost:${port}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { connectionTestSuite } from "data/connection/connection-test-suite";
|
||||||
|
import { bunSqlite } from "./BunSqliteConnection";
|
||||||
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
|
import { describe } from "bun:test";
|
||||||
|
import { Database } from "bun:sqlite";
|
||||||
|
|
||||||
|
describe("BunSqliteConnection", () => {
|
||||||
|
connectionTestSuite(bunTestRunner, {
|
||||||
|
makeConnection: () => ({
|
||||||
|
connection: bunSqlite({ database: new Database(":memory:") }),
|
||||||
|
dispose: async () => {},
|
||||||
|
}),
|
||||||
|
rawDialectDetails: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Database } from "bun:sqlite";
|
||||||
|
import { genericSqlite, type GenericSqliteConnection } from "bknd";
|
||||||
|
|
||||||
|
export type BunSqliteConnection = GenericSqliteConnection<Database>;
|
||||||
|
export type BunSqliteConnectionConfig = {
|
||||||
|
database: Database;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function bunSqlite(config?: BunSqliteConnectionConfig | { url: string }) {
|
||||||
|
let db: Database;
|
||||||
|
if (config) {
|
||||||
|
if ("database" in config) {
|
||||||
|
db = config.database;
|
||||||
|
} else {
|
||||||
|
db = new Database(config.url);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
db = new Database(":memory:");
|
||||||
|
}
|
||||||
|
|
||||||
|
return genericSqlite("bun-sqlite", db, (utils) => {
|
||||||
|
//const fn = cache ? "query" : "prepare";
|
||||||
|
const getStmt = (sql: string) => db.prepare(sql);
|
||||||
|
|
||||||
|
return {
|
||||||
|
db,
|
||||||
|
query: utils.buildQueryFn({
|
||||||
|
all: (sql, parameters) => getStmt(sql).all(...(parameters || [])),
|
||||||
|
run: (sql, parameters) => {
|
||||||
|
const { changes, lastInsertRowid } = getStmt(sql).run(...(parameters || []));
|
||||||
|
return {
|
||||||
|
insertId: utils.parseBigInt(lastInsertRowid),
|
||||||
|
numAffectedRows: utils.parseBigInt(changes),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
close: () => db.close(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1 +1,3 @@
|
|||||||
export * from "./bun.adapter";
|
export * from "./bun.adapter";
|
||||||
|
export * from "../node/storage";
|
||||||
|
export * from "./connection/BunSqliteConnection";
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { expect, test, mock } from "bun:test";
|
import { expect, test, mock, describe, beforeEach, afterEach, afterAll } from "bun:test";
|
||||||
|
|
||||||
export const bunTestRunner = {
|
export const bunTestRunner = {
|
||||||
|
describe,
|
||||||
expect,
|
expect,
|
||||||
test,
|
test,
|
||||||
mock,
|
mock,
|
||||||
|
beforeEach,
|
||||||
|
afterEach,
|
||||||
|
afterAll,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { inspect } from "node:util";
|
||||||
|
|
||||||
export type BindingTypeMap = {
|
export type BindingTypeMap = {
|
||||||
D1Database: D1Database;
|
D1Database: D1Database;
|
||||||
KVNamespace: KVNamespace;
|
KVNamespace: KVNamespace;
|
||||||
@@ -12,7 +14,11 @@ export function getBindings<T extends GetBindingType>(env: any, type: T): Bindin
|
|||||||
const bindings: BindingMap<T>[] = [];
|
const bindings: BindingMap<T>[] = [];
|
||||||
for (const key in env) {
|
for (const key in env) {
|
||||||
try {
|
try {
|
||||||
if (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({
|
bindings.push({
|
||||||
key,
|
key,
|
||||||
value: env[key] as BindingTypeMap[T],
|
value: env[key] as BindingTypeMap[T],
|
||||||
|
|||||||
@@ -13,30 +13,32 @@ describe("cf adapter", () => {
|
|||||||
const DB_URL = ":memory:";
|
const DB_URL = ":memory:";
|
||||||
const $ctx = (env?: any, request?: Request, ctx?: ExecutionContext) => ({
|
const $ctx = (env?: any, request?: Request, ctx?: ExecutionContext) => ({
|
||||||
request: request ?? (null as any),
|
request: request ?? (null as any),
|
||||||
env: env ?? { DB_URL },
|
env: env ?? { url: DB_URL },
|
||||||
ctx: ctx ?? (null as any),
|
ctx: ctx ?? (null as any),
|
||||||
});
|
});
|
||||||
|
|
||||||
it("makes config", async () => {
|
it("makes config", async () => {
|
||||||
expect(
|
const staticConfig = await makeConfig(
|
||||||
makeConfig(
|
{
|
||||||
{
|
connection: { url: DB_URL },
|
||||||
connection: { url: DB_URL },
|
initialConfig: { data: { basepath: DB_URL } },
|
||||||
},
|
},
|
||||||
$ctx({ DB_URL }),
|
$ctx({ DB_URL }),
|
||||||
),
|
);
|
||||||
).toEqual({ connection: { url: DB_URL } });
|
expect(staticConfig.initialConfig).toEqual({ data: { basepath: DB_URL } });
|
||||||
|
expect(staticConfig.connection).toBeDefined();
|
||||||
|
|
||||||
expect(
|
const dynamicConfig = await makeConfig(
|
||||||
makeConfig(
|
{
|
||||||
{
|
app: (env) => ({
|
||||||
app: (env) => ({
|
initialConfig: { data: { basepath: env.DB_URL } },
|
||||||
connection: { url: env.DB_URL },
|
connection: { url: env.DB_URL },
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
$ctx({ DB_URL }),
|
$ctx({ DB_URL }),
|
||||||
),
|
);
|
||||||
).toEqual({ connection: { url: DB_URL } });
|
expect(dynamicConfig.initialConfig).toEqual({ data: { basepath: DB_URL } });
|
||||||
|
expect(dynamicConfig.connection).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
adapterTestSuite<CloudflareBkndConfig, CfMakeConfigArgs<any>>(bunTestRunner, {
|
adapterTestSuite<CloudflareBkndConfig, CfMakeConfigArgs<any>>(bunTestRunner, {
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ import { Hono } from "hono";
|
|||||||
import { serveStatic } from "hono/cloudflare-workers";
|
import { serveStatic } from "hono/cloudflare-workers";
|
||||||
import { getFresh } from "./modes/fresh";
|
import { getFresh } from "./modes/fresh";
|
||||||
import { getCached } from "./modes/cached";
|
import { getCached } from "./modes/cached";
|
||||||
import { getDurable } from "./modes/durable";
|
import type { App, MaybePromise } from "bknd";
|
||||||
import type { App } from "bknd";
|
import { $console } from "bknd/utils";
|
||||||
import { $console } from "core";
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace Cloudflare {
|
namespace Cloudflare {
|
||||||
@@ -17,12 +16,11 @@ declare global {
|
|||||||
|
|
||||||
export type CloudflareEnv = Cloudflare.Env;
|
export type CloudflareEnv = Cloudflare.Env;
|
||||||
export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> & {
|
export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> & {
|
||||||
mode?: "warm" | "fresh" | "cache" | "durable";
|
mode?: "warm" | "fresh" | "cache";
|
||||||
bindings?: (args: Env) => {
|
bindings?: (args: Env) => MaybePromise<{
|
||||||
kv?: KVNamespace;
|
kv?: KVNamespace;
|
||||||
dobj?: DurableObjectNamespace;
|
|
||||||
db?: D1Database;
|
db?: D1Database;
|
||||||
};
|
}>;
|
||||||
d1?: {
|
d1?: {
|
||||||
session?: boolean;
|
session?: boolean;
|
||||||
transport?: "header" | "cookie";
|
transport?: "header" | "cookie";
|
||||||
@@ -33,6 +31,7 @@ export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> &
|
|||||||
keepAliveSeconds?: number;
|
keepAliveSeconds?: number;
|
||||||
forceHttps?: boolean;
|
forceHttps?: boolean;
|
||||||
manifest?: string;
|
manifest?: string;
|
||||||
|
registerMedia?: boolean | ((env: Env) => void);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Context<Env = CloudflareEnv> = {
|
export type Context<Env = CloudflareEnv> = {
|
||||||
@@ -92,8 +91,6 @@ export function serve<Env extends CloudflareEnv = CloudflareEnv>(
|
|||||||
case "cache":
|
case "cache":
|
||||||
app = await getCached(config, context);
|
app = await getCached(config, context);
|
||||||
break;
|
break;
|
||||||
case "durable":
|
|
||||||
return await getDurable(config, context);
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown mode ${mode}`);
|
throw new Error(`Unknown mode ${mode}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
/// <reference types="@cloudflare/workers-types" />
|
/// <reference types="@cloudflare/workers-types" />
|
||||||
|
|
||||||
|
import { Connection } from "bknd";
|
||||||
|
import { sqlite } from "bknd/adapter/sqlite";
|
||||||
|
import { makeConfig as makeAdapterConfig } from "bknd/adapter";
|
||||||
import { registerMedia } from "./storage/StorageR2Adapter";
|
import { registerMedia } from "./storage/StorageR2Adapter";
|
||||||
import { getBinding } from "./bindings";
|
import { getBinding } from "./bindings";
|
||||||
import { D1Connection } from "./connection/D1Connection";
|
import { d1Sqlite } from "./connection/D1Connection";
|
||||||
import type { CloudflareBkndConfig, CloudflareEnv } from ".";
|
import type { CloudflareBkndConfig, CloudflareEnv } from ".";
|
||||||
import { App } from "bknd";
|
import { App } from "bknd";
|
||||||
import { makeConfig as makeAdapterConfig } from "bknd/adapter";
|
|
||||||
import type { Context, ExecutionContext } from "hono";
|
import type { Context, ExecutionContext } from "hono";
|
||||||
import { $console } from "core";
|
import { $console } from "bknd/utils";
|
||||||
import { setCookie } from "hono/cookie";
|
import { setCookie } from "hono/cookie";
|
||||||
|
|
||||||
export const constants = {
|
export const constants = {
|
||||||
@@ -87,65 +89,88 @@ export function d1SessionHelper(config: CloudflareBkndConfig<any>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let media_registered: boolean = false;
|
let media_registered: boolean = false;
|
||||||
export function makeConfig<Env extends CloudflareEnv = CloudflareEnv>(
|
export async function makeConfig<Env extends CloudflareEnv = CloudflareEnv>(
|
||||||
config: CloudflareBkndConfig<Env>,
|
config: CloudflareBkndConfig<Env>,
|
||||||
args?: CfMakeConfigArgs<Env>,
|
args?: CfMakeConfigArgs<Env>,
|
||||||
) {
|
) {
|
||||||
if (!media_registered) {
|
if (!media_registered && config.registerMedia !== false) {
|
||||||
registerMedia(args?.env as any);
|
if (typeof config.registerMedia === "function") {
|
||||||
|
config.registerMedia(args?.env as any);
|
||||||
|
} else {
|
||||||
|
registerMedia(args?.env as any);
|
||||||
|
}
|
||||||
media_registered = true;
|
media_registered = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const appConfig = makeAdapterConfig(config, args?.env);
|
const appConfig = await makeAdapterConfig(config, args?.env);
|
||||||
|
|
||||||
if (args?.env) {
|
// if connection instance is given, don't do anything
|
||||||
const bindings = config.bindings?.(args?.env);
|
// 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 sessionHelper = d1SessionHelper(config);
|
||||||
const sessionId = sessionHelper.get(args.request);
|
const sessionId = sessionHelper.get(args.request);
|
||||||
let session: D1DatabaseSession | undefined;
|
let session: D1DatabaseSession | undefined;
|
||||||
|
let db: D1Database | undefined;
|
||||||
|
|
||||||
if (!appConfig.connection) {
|
// if db is given in bindings, use it
|
||||||
let db: D1Database | undefined;
|
if (bindings?.db) {
|
||||||
if (bindings?.db) {
|
$console.debug("Using database from bindings");
|
||||||
$console.log("Using database from bindings");
|
db = bindings.db;
|
||||||
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) {
|
// scan for D1Database in args
|
||||||
if (config.d1?.session) {
|
} else {
|
||||||
session = db.withSession(sessionId ?? config.d1?.first);
|
const binding = getBinding(args.env, "D1Database");
|
||||||
appConfig.connection = new D1Connection({ binding: session });
|
if (binding) {
|
||||||
} else {
|
$console.debug(`Using database from env "${binding.key}"`);
|
||||||
appConfig.connection = new D1Connection({ binding: db });
|
db = binding.value;
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error("No database connection given");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.d1?.session) {
|
// if db is found, check if session is requested
|
||||||
appConfig.options = {
|
if (db) {
|
||||||
...appConfig.options,
|
if (config.d1?.session) {
|
||||||
manager: {
|
session = db.withSession(sessionId ?? config.d1?.first);
|
||||||
...appConfig.options?.manager,
|
if (!session) {
|
||||||
onServerInit: (server) => {
|
throw new Error("Couldn't create session");
|
||||||
server.use(async (c, next) => {
|
}
|
||||||
sessionHelper.set(c, session);
|
|
||||||
await next();
|
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;
|
return appConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,65 +1,75 @@
|
|||||||
/// <reference types="@cloudflare/workers-types" />
|
/// <reference types="@cloudflare/workers-types" />
|
||||||
|
|
||||||
import { KyselyPluginRunner, SqliteConnection, SqliteIntrospector } from "bknd/data";
|
import { genericSqlite, type GenericSqliteConnection } from "bknd";
|
||||||
import type { QB } from "data/connection/Connection";
|
import type { QueryResult } from "kysely";
|
||||||
import { type DatabaseIntrospector, Kysely, ParseJSONResultsPlugin } from "kysely";
|
|
||||||
import { D1Dialect } from "kysely-d1";
|
export type D1SqliteConnection = GenericSqliteConnection<D1Database>;
|
||||||
|
|
||||||
export type D1ConnectionConfig<DB extends D1Database | D1DatabaseSession = D1Database> = {
|
export type D1ConnectionConfig<DB extends D1Database | D1DatabaseSession = D1Database> = {
|
||||||
binding: DB;
|
binding: DB;
|
||||||
};
|
};
|
||||||
|
|
||||||
class CustomD1Dialect extends D1Dialect {
|
export function d1Sqlite<DB extends D1Database | D1DatabaseSession = D1Database>(
|
||||||
override createIntrospector(db: Kysely<any>): DatabaseIntrospector {
|
config: D1ConnectionConfig<DB>,
|
||||||
return new SqliteIntrospector(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"],
|
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"],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/// <reference types="@cloudflare/workers-types" />
|
||||||
|
|
||||||
|
import { describe, test, expect } from "vitest";
|
||||||
|
|
||||||
|
import { viTestRunner } from "adapter/node/vitest";
|
||||||
|
import { connectionTestSuite } from "data/connection/connection-test-suite";
|
||||||
|
import { Miniflare } from "miniflare";
|
||||||
|
import { doSqlite } from "./DoConnection";
|
||||||
|
|
||||||
|
const script = `
|
||||||
|
import { DurableObject } from "cloudflare:workers";
|
||||||
|
|
||||||
|
export class TestObject extends DurableObject {
|
||||||
|
constructor(ctx, env) {
|
||||||
|
super(ctx, env);
|
||||||
|
this.storage = ctx.storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
async exec(sql, ...parameters) {
|
||||||
|
//return { sql, parameters }
|
||||||
|
const cursor = this.storage.sql.exec(sql, ...parameters);
|
||||||
|
return {
|
||||||
|
rows: cursor.toArray() || [],
|
||||||
|
rowsWritten: cursor.rowsWritten,
|
||||||
|
rowsRead: cursor.rowsRead,
|
||||||
|
databaseSize: this.storage.sql.databaseSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async databaseSize() {
|
||||||
|
return this.storage.sql.databaseSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async fetch(request, env) {
|
||||||
|
const stub = env.TEST_OBJECT.get(env.TEST_OBJECT.idFromName("test"));
|
||||||
|
return stub.fetch(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
describe("doSqlite", async () => {
|
||||||
|
connectionTestSuite(viTestRunner, {
|
||||||
|
makeConnection: async () => {
|
||||||
|
const mf = new Miniflare({
|
||||||
|
modules: true,
|
||||||
|
durableObjects: { TEST_OBJECT: { className: "TestObject", useSQLite: true } },
|
||||||
|
script,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ns = await mf.getDurableObjectNamespace("TEST_OBJECT");
|
||||||
|
const id = ns.idFromName("test");
|
||||||
|
const stub = ns.get(id) as unknown as DurableObjectStub<
|
||||||
|
Rpc.DurableObjectBranded & {
|
||||||
|
exec: (sql: string, ...parameters: any[]) => Promise<any>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
const stubs: any[] = [];
|
||||||
|
const mock = {
|
||||||
|
databaseSize: 0,
|
||||||
|
exec: async function (sql: string, ...parameters: any[]) {
|
||||||
|
// @ts-ignore
|
||||||
|
const result = (await stub.exec(sql, ...parameters)) as any;
|
||||||
|
this.databaseSize = result.databaseSize;
|
||||||
|
stubs.push(result);
|
||||||
|
return {
|
||||||
|
toArray: () => result.rows,
|
||||||
|
rowsWritten: result.rowsWritten,
|
||||||
|
rowsRead: result.rowsRead,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
connection: doSqlite({ sql: mock as any }),
|
||||||
|
dispose: async () => {
|
||||||
|
await Promise.all(
|
||||||
|
stubs.map((stub) => {
|
||||||
|
try {
|
||||||
|
return stub[Symbol.dispose]();
|
||||||
|
} catch (e) {}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await mf.dispose();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
rawDialectDetails: ["meta.rowsWritten", "meta.rowsRead", "meta.databaseSize"],
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import type { ICacheDriver } from "core/drivers";
|
||||||
|
|
||||||
|
interface WorkersKVCacheOptions {
|
||||||
|
// default time-to-live in seconds
|
||||||
|
defaultTTL?: number;
|
||||||
|
// prefix for the cache key
|
||||||
|
cachePrefix?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WorkersKVCacheDriver implements ICacheDriver {
|
||||||
|
protected readonly kv: KVNamespace;
|
||||||
|
protected readonly defaultTTL?: number;
|
||||||
|
protected readonly cachePrefix: string;
|
||||||
|
|
||||||
|
constructor(kv: KVNamespace, options: WorkersKVCacheOptions = {}) {
|
||||||
|
this.kv = kv;
|
||||||
|
this.cachePrefix = options.cachePrefix ?? "";
|
||||||
|
this.defaultTTL = options.defaultTTL;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getKey(key: string): string {
|
||||||
|
return this.cachePrefix + key;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(key: string): Promise<string | undefined> {
|
||||||
|
const value = await this.kv.get(this.getKey(key));
|
||||||
|
return value === null ? undefined : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async set(key: string, value: string, ttl?: number): Promise<void> {
|
||||||
|
let expirationTtl = ttl ?? this.defaultTTL;
|
||||||
|
if (expirationTtl) {
|
||||||
|
expirationTtl = Math.max(expirationTtl, 60);
|
||||||
|
}
|
||||||
|
await this.kv.put(this.getKey(key), value, { expirationTtl: expirationTtl });
|
||||||
|
}
|
||||||
|
|
||||||
|
async del(key: string): Promise<void> {
|
||||||
|
await this.kv.delete(this.getKey(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cacheWorkersKV = (kv: KVNamespace, options?: WorkersKVCacheOptions) => {
|
||||||
|
return new WorkersKVCacheDriver(kv, options);
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { describe, vi, afterAll, beforeAll } from "vitest";
|
||||||
|
import { cacheWorkersKV } from "./cache";
|
||||||
|
import { viTestRunner } from "adapter/node/vitest";
|
||||||
|
import { cacheDriverTestSuite } from "core/drivers/cache/cache-driver-test-suite";
|
||||||
|
import { Miniflare } from "miniflare";
|
||||||
|
|
||||||
|
describe("cacheWorkersKV", async () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
});
|
||||||
|
afterAll(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
const mf = new Miniflare({
|
||||||
|
modules: true,
|
||||||
|
script: "export default { async fetch() { return new Response(null); } }",
|
||||||
|
kvNamespaces: ["KV"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const kv = (await mf.getKVNamespace("KV")) as unknown as KVNamespace;
|
||||||
|
|
||||||
|
cacheDriverTestSuite(viTestRunner, {
|
||||||
|
makeCache: () => cacheWorkersKV(kv),
|
||||||
|
setTime: (ms: number) => {
|
||||||
|
vi.advanceTimersByTime(ms);
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
minTTL: 60,
|
||||||
|
// doesn't work with miniflare
|
||||||
|
skipTTL: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { D1Connection, type D1ConnectionConfig } from "./connection/D1Connection";
|
import { d1Sqlite, type D1ConnectionConfig } from "./connection/D1Connection";
|
||||||
|
|
||||||
export * from "./cloudflare-workers.adapter";
|
export * from "./cloudflare-workers.adapter";
|
||||||
export { makeApp, getFresh } from "./modes/fresh";
|
export { makeApp, getFresh } from "./modes/fresh";
|
||||||
export { getCached } from "./modes/cached";
|
export { getCached } from "./modes/cached";
|
||||||
export { DurableBkndApp, getDurable } from "./modes/durable";
|
export { d1Sqlite, type D1ConnectionConfig };
|
||||||
export { D1Connection, type D1ConnectionConfig };
|
export { doSqlite, type DoConnectionConfig } from "./connection/DoConnection";
|
||||||
export {
|
export {
|
||||||
getBinding,
|
getBinding,
|
||||||
getBindings,
|
getBindings,
|
||||||
@@ -13,7 +13,13 @@ export {
|
|||||||
type BindingMap,
|
type BindingMap,
|
||||||
} from "./bindings";
|
} from "./bindings";
|
||||||
export { constants } from "./config";
|
export { constants } from "./config";
|
||||||
|
export { StorageR2Adapter, registerMedia } from "./storage/StorageR2Adapter";
|
||||||
|
export { registries } from "bknd";
|
||||||
|
export { devFsPlugin, devFsWrite } from "./vite";
|
||||||
|
|
||||||
export function d1(config: D1ConnectionConfig) {
|
// for compatibility with old code
|
||||||
return new D1Connection(config);
|
export function d1<DB extends D1Database | D1DatabaseSession = D1Database>(
|
||||||
|
config: D1ConnectionConfig<DB>,
|
||||||
|
) {
|
||||||
|
return d1Sqlite<DB>(config);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export async function getCached<Env extends CloudflareEnv = CloudflareEnv>(
|
|||||||
args: Context<Env>,
|
args: Context<Env>,
|
||||||
) {
|
) {
|
||||||
const { env, ctx } = args;
|
const { env, ctx } = args;
|
||||||
const { kv } = config.bindings?.(env)!;
|
const { kv } = await config.bindings?.(env)!;
|
||||||
if (!kv) throw new Error("kv namespace is not defined in cloudflare.bindings");
|
if (!kv) throw new Error("kv namespace is not defined in cloudflare.bindings");
|
||||||
const key = config.key ?? "app";
|
const key = config.key ?? "app";
|
||||||
|
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
import { DurableObject } from "cloudflare:workers";
|
|
||||||
import type { App, CreateAppConfig } from "bknd";
|
|
||||||
import { createRuntimeApp, makeConfig } from "bknd/adapter";
|
|
||||||
import type { CloudflareBkndConfig, Context, CloudflareEnv } from "../index";
|
|
||||||
import { constants, registerAsyncsExecutionContext } from "../config";
|
|
||||||
import { $console } from "core";
|
|
||||||
|
|
||||||
export async function getDurable<Env extends CloudflareEnv = CloudflareEnv>(
|
|
||||||
config: CloudflareBkndConfig<Env>,
|
|
||||||
ctx: Context<Env>,
|
|
||||||
) {
|
|
||||||
const { dobj } = config.bindings?.(ctx.env)!;
|
|
||||||
if (!dobj) throw new Error("durable object is not defined in cloudflare.bindings");
|
|
||||||
const key = config.key ?? "app";
|
|
||||||
|
|
||||||
if ([config.onBuilt, config.beforeBuild].some((x) => x)) {
|
|
||||||
$console.warn("onBuilt and beforeBuild are not supported with DurableObject mode");
|
|
||||||
}
|
|
||||||
|
|
||||||
const start = performance.now();
|
|
||||||
|
|
||||||
const id = dobj.idFromName(key);
|
|
||||||
const stub = dobj.get(id) as unknown as DurableBkndApp;
|
|
||||||
|
|
||||||
const create_config = makeConfig(config, ctx.env);
|
|
||||||
|
|
||||||
const res = await stub.fire(ctx.request, {
|
|
||||||
config: create_config,
|
|
||||||
keepAliveSeconds: config.keepAliveSeconds,
|
|
||||||
});
|
|
||||||
|
|
||||||
const headers = new Headers(res.headers);
|
|
||||||
headers.set("X-TTDO", String(performance.now() - start));
|
|
||||||
|
|
||||||
return new Response(res.body, {
|
|
||||||
status: res.status,
|
|
||||||
statusText: res.statusText,
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DurableBkndApp extends DurableObject {
|
|
||||||
protected id = Math.random().toString(36).slice(2);
|
|
||||||
protected app?: App;
|
|
||||||
protected interval?: any;
|
|
||||||
|
|
||||||
async fire(
|
|
||||||
request: Request,
|
|
||||||
options: {
|
|
||||||
config: CreateAppConfig;
|
|
||||||
html?: string;
|
|
||||||
keepAliveSeconds?: number;
|
|
||||||
setAdminHtml?: boolean;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
let buildtime = 0;
|
|
||||||
if (!this.app) {
|
|
||||||
const start = performance.now();
|
|
||||||
const config = options.config;
|
|
||||||
|
|
||||||
// change protocol to websocket if libsql
|
|
||||||
if (
|
|
||||||
config?.connection &&
|
|
||||||
"type" in config.connection &&
|
|
||||||
config.connection.type === "libsql"
|
|
||||||
) {
|
|
||||||
config.connection.config.protocol = "wss";
|
|
||||||
}
|
|
||||||
|
|
||||||
this.app = await createRuntimeApp({
|
|
||||||
...config,
|
|
||||||
onBuilt: async (app) => {
|
|
||||||
registerAsyncsExecutionContext(app, this.ctx);
|
|
||||||
app.modules.server.get(constants.do_endpoint, async (c) => {
|
|
||||||
// @ts-ignore
|
|
||||||
const context: any = c.req.raw.cf ? c.req.raw.cf : c.env.cf;
|
|
||||||
return c.json({
|
|
||||||
id: this.id,
|
|
||||||
keepAliveSeconds: options?.keepAliveSeconds ?? 0,
|
|
||||||
colo: context.colo,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.onBuilt(app);
|
|
||||||
},
|
|
||||||
adminOptions: { html: options.html },
|
|
||||||
beforeBuild: async (app) => {
|
|
||||||
await this.beforeBuild(app);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
buildtime = performance.now() - start;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options?.keepAliveSeconds) {
|
|
||||||
this.keepAlive(options.keepAliveSeconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await this.app!.fetch(request);
|
|
||||||
const headers = new Headers(res.headers);
|
|
||||||
headers.set("X-BuildTime", buildtime.toString());
|
|
||||||
headers.set("X-DO-ID", this.id);
|
|
||||||
|
|
||||||
return new Response(res.body, {
|
|
||||||
status: res.status,
|
|
||||||
statusText: res.statusText,
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async onBuilt(app: App) {}
|
|
||||||
|
|
||||||
async beforeBuild(app: App) {}
|
|
||||||
|
|
||||||
protected keepAlive(seconds: number) {
|
|
||||||
if (this.interval) {
|
|
||||||
clearInterval(this.interval);
|
|
||||||
}
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
this.interval = setInterval(() => {
|
|
||||||
i += 1;
|
|
||||||
if (i === seconds) {
|
|
||||||
console.log("cleared");
|
|
||||||
clearInterval(this.interval);
|
|
||||||
|
|
||||||
// ping every 30 seconds
|
|
||||||
} else if (i % 30 === 0) {
|
|
||||||
console.log("ping");
|
|
||||||
this.app?.modules.ctx().connection.ping();
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@ export async function makeApp<Env extends CloudflareEnv = CloudflareEnv>(
|
|||||||
args?: CfMakeConfigArgs<Env>,
|
args?: CfMakeConfigArgs<Env>,
|
||||||
opts?: RuntimeOptions,
|
opts?: RuntimeOptions,
|
||||||
) {
|
) {
|
||||||
return await createRuntimeApp<Env>(makeConfig(config, args), args?.env, opts);
|
return await createRuntimeApp<Env>(await makeConfig(config, args), args?.env, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFresh<Env extends CloudflareEnv = CloudflareEnv>(
|
export async function getFresh<Env extends CloudflareEnv = CloudflareEnv>(
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import {
|
||||||
|
d1Sqlite,
|
||||||
|
getBinding,
|
||||||
|
registerMedia,
|
||||||
|
type CloudflareBkndConfig,
|
||||||
|
type CloudflareEnv,
|
||||||
|
} from "bknd/adapter/cloudflare";
|
||||||
|
import type { PlatformProxy } from "wrangler";
|
||||||
|
import process from "node:process";
|
||||||
|
|
||||||
|
export type WithPlatformProxyOptions = {
|
||||||
|
/**
|
||||||
|
* By default, proxy is used if the PROXY environment variable is set to 1.
|
||||||
|
* You can override/force this by setting this option.
|
||||||
|
*/
|
||||||
|
useProxy?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function withPlatformProxy<Env extends CloudflareEnv>(
|
||||||
|
config?: CloudflareBkndConfig<Env>,
|
||||||
|
opts?: WithPlatformProxyOptions,
|
||||||
|
) {
|
||||||
|
const use_proxy =
|
||||||
|
typeof opts?.useProxy === "boolean" ? opts.useProxy : process.env.PROXY === "1";
|
||||||
|
let proxy: PlatformProxy | undefined;
|
||||||
|
|
||||||
|
async function getEnv(env?: Env): Promise<Env> {
|
||||||
|
if (use_proxy) {
|
||||||
|
if (!proxy) {
|
||||||
|
const getPlatformProxy = await import("wrangler").then((mod) => mod.getPlatformProxy);
|
||||||
|
proxy = await getPlatformProxy();
|
||||||
|
setTimeout(proxy?.dispose, 1000);
|
||||||
|
}
|
||||||
|
return proxy.env as unknown as Env;
|
||||||
|
}
|
||||||
|
return env || ({} as Env);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
beforeBuild: async (app, registries) => {
|
||||||
|
if (!use_proxy) return;
|
||||||
|
const env = await getEnv();
|
||||||
|
registerMedia(env, registries as any);
|
||||||
|
await config?.beforeBuild?.(app, registries);
|
||||||
|
},
|
||||||
|
bindings: async (env) => {
|
||||||
|
return (await config?.bindings?.(await getEnv(env))) || {};
|
||||||
|
},
|
||||||
|
// @ts-ignore
|
||||||
|
app: async (_env) => {
|
||||||
|
const env = await getEnv(_env);
|
||||||
|
|
||||||
|
if (config?.app === undefined && use_proxy) {
|
||||||
|
const binding = getBinding(env, "D1Database");
|
||||||
|
return {
|
||||||
|
connection: d1Sqlite({
|
||||||
|
binding: binding.value,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
} else if (typeof config?.app === "function") {
|
||||||
|
return config?.app(env);
|
||||||
|
}
|
||||||
|
return config?.app || {};
|
||||||
|
},
|
||||||
|
} satisfies CloudflareBkndConfig<Env>;
|
||||||
|
}
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { createWriteStream, readFileSync } from "node:fs";
|
|
||||||
import { test } from "node:test";
|
|
||||||
import { Miniflare } from "miniflare";
|
|
||||||
import { StorageR2Adapter } from "./StorageR2Adapter";
|
|
||||||
import { adapterTestSuite } from "media";
|
|
||||||
import { nodeTestRunner } from "adapter/node/test";
|
|
||||||
import path from "node:path";
|
|
||||||
|
|
||||||
// https://github.com/nodejs/node/issues/44372#issuecomment-1736530480
|
|
||||||
console.log = async (message: any) => {
|
|
||||||
const tty = createWriteStream("/dev/tty");
|
|
||||||
const msg = typeof message === "string" ? message : JSON.stringify(message, null, 2);
|
|
||||||
return tty.write(`${msg}\n`);
|
|
||||||
};
|
|
||||||
|
|
||||||
test("StorageR2Adapter", async () => {
|
|
||||||
const mf = new Miniflare({
|
|
||||||
modules: true,
|
|
||||||
script: "export default { async fetch() { return new Response(null); } }",
|
|
||||||
r2Buckets: ["BUCKET"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const bucket = (await mf.getR2Bucket("BUCKET")) as unknown as R2Bucket;
|
|
||||||
const adapter = new StorageR2Adapter(bucket);
|
|
||||||
|
|
||||||
const basePath = path.resolve(import.meta.dirname, "../../../../__test__/_assets");
|
|
||||||
const buffer = readFileSync(path.join(basePath, "image.png"));
|
|
||||||
const file = new File([buffer], "image.png", { type: "image/png" });
|
|
||||||
|
|
||||||
await adapterTestSuite(nodeTestRunner, adapter, file);
|
|
||||||
await mf.dispose();
|
|
||||||
});
|
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
import { registries } from "bknd";
|
import { registries as $registries, isDebug, guessMimeType } from "bknd";
|
||||||
import { isDebug } from "bknd/core";
|
|
||||||
import { StringEnum } from "bknd/utils";
|
|
||||||
import { guessMimeType as guess, StorageAdapter, type FileBody } from "bknd/media";
|
|
||||||
import { getBindings } from "../bindings";
|
import { getBindings } from "../bindings";
|
||||||
import * as tb from "@sinclair/typebox";
|
import { s } from "bknd/utils";
|
||||||
const { Type } = tb;
|
import { StorageAdapter, type FileBody } from "bknd";
|
||||||
|
|
||||||
export function makeSchema(bindings: string[] = []) {
|
export function makeSchema(bindings: string[] = []) {
|
||||||
return Type.Object(
|
return s.object(
|
||||||
{
|
{
|
||||||
binding: bindings.length > 0 ? StringEnum(bindings) : Type.Optional(Type.String()),
|
binding: bindings.length > 0 ? s.string({ enum: bindings }) : s.string().optional(),
|
||||||
},
|
},
|
||||||
{ title: "R2", description: "Cloudflare R2 storage" },
|
{ title: "R2", description: "Cloudflare R2 storage" },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerMedia(env: Record<string, any>) {
|
export function registerMedia(
|
||||||
|
env: Record<string, any>,
|
||||||
|
registries: typeof $registries = $registries,
|
||||||
|
) {
|
||||||
const r2_bindings = getBindings(env, "R2Bucket");
|
const r2_bindings = getBindings(env, "R2Bucket");
|
||||||
|
|
||||||
registries.media.register(
|
registries.media.register(
|
||||||
@@ -63,46 +63,49 @@ export class StorageR2Adapter extends StorageAdapter {
|
|||||||
|
|
||||||
async putObject(key: string, body: FileBody) {
|
async putObject(key: string, body: FileBody) {
|
||||||
try {
|
try {
|
||||||
const res = await this.bucket.put(key, body);
|
const res = await this.bucket.put(this.getKey(key), body);
|
||||||
return res?.etag;
|
return res?.etag;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async listObjects(
|
async listObjects(prefix = ""): Promise<{ key: string; last_modified: Date; size: number }[]> {
|
||||||
prefix?: string,
|
const list = await this.bucket.list({ limit: 50, prefix: this.getKey(prefix) });
|
||||||
): Promise<{ key: string; last_modified: Date; size: number }[]> {
|
|
||||||
const list = await this.bucket.list({ limit: 50 });
|
|
||||||
return list.objects.map((item) => ({
|
return list.objects.map((item) => ({
|
||||||
key: item.key,
|
key: item.key.replace(this.getKey(""), ""),
|
||||||
size: item.size,
|
size: item.size,
|
||||||
last_modified: item.uploaded,
|
last_modified: item.uploaded,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async headObject(key: string): Promise<R2Object | null> {
|
private async headObject(key: string): Promise<R2Object | null> {
|
||||||
return await this.bucket.head(key);
|
return await this.bucket.head(this.getKey(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
async objectExists(key: string): Promise<boolean> {
|
async objectExists(key: string): Promise<boolean> {
|
||||||
return (await this.headObject(key)) !== null;
|
return (await this.headObject(key)) !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getObject(key: string, headers: Headers): Promise<Response> {
|
async getObject(_key: string, headers: Headers): Promise<Response> {
|
||||||
let object: R2ObjectBody | null;
|
let object: R2ObjectBody | null;
|
||||||
|
const key = this.getKey(_key);
|
||||||
|
|
||||||
const responseHeaders = new Headers({
|
const responseHeaders = new Headers({
|
||||||
"Accept-Ranges": "bytes",
|
"Accept-Ranges": "bytes",
|
||||||
"Content-Type": guess(key),
|
"Content-Type": guessMimeType(key),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const range = headers.has("range");
|
||||||
|
|
||||||
//console.log("getObject:headers", headersToObject(headers));
|
//console.log("getObject:headers", headersToObject(headers));
|
||||||
if (headers.has("range")) {
|
if (range) {
|
||||||
const options = isDebug()
|
const options = isDebug()
|
||||||
? {} // miniflare doesn't support range requests
|
? {} // miniflare doesn't support range requests
|
||||||
: {
|
: {
|
||||||
range: headers,
|
range: headers,
|
||||||
onlyIf: headers,
|
onlyIf: headers,
|
||||||
};
|
};
|
||||||
|
|
||||||
object = (await this.bucket.get(key, options)) as R2ObjectBody;
|
object = (await this.bucket.get(key, options)) as R2ObjectBody;
|
||||||
|
|
||||||
if (!object) {
|
if (!object) {
|
||||||
@@ -130,17 +133,18 @@ export class StorageR2Adapter extends StorageAdapter {
|
|||||||
responseHeaders.set("Last-Modified", object.uploaded.toUTCString());
|
responseHeaders.set("Last-Modified", object.uploaded.toUTCString());
|
||||||
|
|
||||||
return new Response(object.body, {
|
return new Response(object.body, {
|
||||||
status: object.range ? 206 : 200,
|
status: range ? 206 : 200,
|
||||||
headers: responseHeaders,
|
headers: responseHeaders,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private writeHttpMetadata(headers: Headers, object: R2Object | R2ObjectBody): void {
|
private writeHttpMetadata(headers: Headers, object: R2Object | R2ObjectBody): void {
|
||||||
let metadata = object.httpMetadata;
|
let metadata = object.httpMetadata;
|
||||||
|
|
||||||
if (!metadata || Object.keys(metadata).length === 0) {
|
if (!metadata || Object.keys(metadata).length === 0) {
|
||||||
// guessing is especially required for dev environment (miniflare)
|
// guessing is especially required for dev environment (miniflare)
|
||||||
metadata = {
|
metadata = {
|
||||||
contentType: guess(object.key),
|
contentType: guessMimeType(object.key),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,19 +161,23 @@ export class StorageR2Adapter extends StorageAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: String(head.httpMetadata?.contentType ?? guess(key)),
|
type: String(head.httpMetadata?.contentType ?? guessMimeType(key)),
|
||||||
size: head.size,
|
size: head.size,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteObject(key: string): Promise<void> {
|
async deleteObject(key: string): Promise<void> {
|
||||||
await this.bucket.delete(key);
|
await this.bucket.delete(this.getKey(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
getObjectUrl(key: string): string {
|
getObjectUrl(key: string): string {
|
||||||
throw new Error("Method getObjectUrl not implemented.");
|
throw new Error("Method getObjectUrl not implemented.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected getKey(key: string) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
toJSON(secrets?: boolean) {
|
toJSON(secrets?: boolean) {
|
||||||
return {
|
return {
|
||||||
type: this.getName(),
|
type: this.getName(),
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { Miniflare } from "miniflare";
|
||||||
|
import { StorageR2Adapter } from "./StorageR2Adapter";
|
||||||
|
import { adapterTestSuite } from "media/storage/adapters/adapter-test-suite";
|
||||||
|
import path from "node:path";
|
||||||
|
import { describe, afterAll, test, expect } from "vitest";
|
||||||
|
import { viTestRunner } from "adapter/node/vitest";
|
||||||
|
|
||||||
|
let mf: Miniflare | undefined;
|
||||||
|
describe("StorageR2Adapter", async () => {
|
||||||
|
mf = new Miniflare({
|
||||||
|
modules: true,
|
||||||
|
script: "export default { async fetch() { return new Response(null); } }",
|
||||||
|
r2Buckets: ["BUCKET"],
|
||||||
|
});
|
||||||
|
const bucket = (await mf?.getR2Bucket("BUCKET")) as unknown as R2Bucket;
|
||||||
|
|
||||||
|
test("test", () => {
|
||||||
|
expect(bucket).toBeDefined();
|
||||||
|
});
|
||||||
|
const adapter = new StorageR2Adapter(bucket);
|
||||||
|
|
||||||
|
const basePath = path.resolve(import.meta.dirname, "../../../../__test__/_assets");
|
||||||
|
const buffer = readFileSync(path.join(basePath, "image.png"));
|
||||||
|
const file = new File([buffer], "image.png", { type: "image/png" });
|
||||||
|
|
||||||
|
await adapterTestSuite(viTestRunner, adapter, file);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await mf?.dispose();
|
||||||
|
});
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import type { Plugin } from "vite";
|
||||||
|
import { writeFile as nodeWriteFile } from "node:fs/promises";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vite plugin that provides Node.js filesystem access during development
|
||||||
|
* by injecting a polyfill into the SSR environment
|
||||||
|
*/
|
||||||
|
export function devFsVitePlugin({
|
||||||
|
verbose = false,
|
||||||
|
configFile = "bknd.config.ts",
|
||||||
|
}: {
|
||||||
|
verbose?: boolean;
|
||||||
|
configFile?: string;
|
||||||
|
}): Plugin {
|
||||||
|
let isDev = false;
|
||||||
|
let projectRoot = "";
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "dev-fs-plugin",
|
||||||
|
enforce: "pre",
|
||||||
|
configResolved(config) {
|
||||||
|
isDev = config.command === "serve";
|
||||||
|
projectRoot = config.root;
|
||||||
|
},
|
||||||
|
configureServer(server) {
|
||||||
|
if (!isDev) return;
|
||||||
|
|
||||||
|
// Intercept stdout to watch for our write requests
|
||||||
|
const originalStdoutWrite = process.stdout.write;
|
||||||
|
process.stdout.write = function (chunk: any, encoding?: any, callback?: any) {
|
||||||
|
const output = chunk.toString();
|
||||||
|
|
||||||
|
// Check if this output contains our special write request
|
||||||
|
if (output.includes("{{DEV_FS_WRITE_REQUEST}}")) {
|
||||||
|
try {
|
||||||
|
// Extract the JSON from the log line
|
||||||
|
const match = output.match(/{{DEV_FS_WRITE_REQUEST}} ({.*})/);
|
||||||
|
if (match) {
|
||||||
|
const writeRequest = JSON.parse(match[1]);
|
||||||
|
if (writeRequest.type === "DEV_FS_WRITE_REQUEST") {
|
||||||
|
if (verbose) {
|
||||||
|
console.debug("[dev-fs-plugin] Intercepted write request via stdout");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the write request immediately
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const fullPath = resolve(projectRoot, writeRequest.filename);
|
||||||
|
await nodeWriteFile(fullPath, writeRequest.data);
|
||||||
|
if (verbose) {
|
||||||
|
console.debug("[dev-fs-plugin] File written successfully!");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[dev-fs-plugin] Error writing file:", error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Don't output the raw write request to console
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Not a valid write request, continue with normal output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
// biome-ignore lint:
|
||||||
|
return originalStdoutWrite.apply(process.stdout, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Restore stdout when server closes
|
||||||
|
server.httpServer?.on("close", () => {
|
||||||
|
process.stdout.write = originalStdoutWrite;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// @ts-ignore
|
||||||
|
transform(code, id, options) {
|
||||||
|
// Only transform in SSR mode during development
|
||||||
|
if (!isDev || !options?.ssr) return;
|
||||||
|
|
||||||
|
// Check if this is the bknd config file
|
||||||
|
if (id.includes(configFile)) {
|
||||||
|
if (verbose) {
|
||||||
|
console.debug("[dev-fs-plugin] Transforming", configFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject our filesystem polyfill at the top of the file
|
||||||
|
const polyfill = `
|
||||||
|
// Dev-fs polyfill injected by vite-plugin-dev-fs
|
||||||
|
if (typeof globalThis !== 'undefined') {
|
||||||
|
globalThis.__devFsPolyfill = {
|
||||||
|
writeFile: async (filename, data) => {
|
||||||
|
${verbose ? "console.debug('dev-fs polyfill: Intercepting write request for', filename);" : ""}
|
||||||
|
|
||||||
|
// Use console logging as a communication channel
|
||||||
|
// The main process will watch for this specific log pattern
|
||||||
|
const writeRequest = {
|
||||||
|
type: 'DEV_FS_WRITE_REQUEST',
|
||||||
|
filename: filename,
|
||||||
|
data: data,
|
||||||
|
timestamp: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Output as a specially formatted console message
|
||||||
|
console.log('{{DEV_FS_WRITE_REQUEST}}', JSON.stringify(writeRequest));
|
||||||
|
${verbose ? "console.debug('dev-fs polyfill: Write request sent via console');" : ""}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
return polyfill + code;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write function that uses the dev-fs polyfill injected by our Vite plugin
|
||||||
|
export async function devFsWrite(filename: string, data: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Check if the dev-fs polyfill is available (injected by our Vite plugin)
|
||||||
|
if (typeof globalThis !== "undefined" && (globalThis as any).__devFsPolyfill) {
|
||||||
|
return (globalThis as any).__devFsPolyfill.writeFile(filename, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to Node.js fs for other environments (Node.js, Bun)
|
||||||
|
const { writeFile } = await import("node:fs/promises");
|
||||||
|
return writeFile(filename, data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[dev-fs-write] Error writing file:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";
|
||||||
|
|
||||||
|
export default defineWorkersConfig({
|
||||||
|
test: {
|
||||||
|
poolOptions: {
|
||||||
|
workers: {
|
||||||
|
miniflare: {
|
||||||
|
compatibilityDate: "2025-06-04",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: ["**/*.vi-test.ts", "**/*.vitest.ts"],
|
||||||
|
},
|
||||||
|
});
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user