mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 16:16:02 +00:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c1e2456ada | |||
| bebf9e0411 | |||
| ca55503e66 | |||
| 1ec3ba0e92 | |||
| ea372a8125 | |||
| 847c264b35 | |||
| b4f024bef8 | |||
| 8bc63ba2a7 | |||
| cecb0e829a | |||
| 491bd65da1 | |||
| ba7fea5571 | |||
| ba7587aa3f | |||
| 0a49061549 | |||
| 682ecc6dac | |||
| efbc5f115f | |||
| b4d6b1c04d | |||
| e21f1a3ee2 | |||
| a9efee3555 | |||
| 2ca1f64ae6 | |||
| 9d5bc61f33 | |||
| ea46dc8df0 | |||
| 88ec3432a0 | |||
| 43c8f14470 | |||
| c384bf4dd4 | |||
| db2a994a01 | |||
| 166ea4a71b | |||
| 0d3bb3b7d6 | |||
| cfbec5b6ea | |||
| 5143ee5726 | |||
| fe1716ed01 | |||
| 0c31dcdb95 | |||
| 4cc0f8e172 | |||
| c161a26ec0 | |||
| 6e78a4c238 | |||
| 42edce904f |
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"bknd": {
|
|
||||||
"url": "http://localhost:28623/api/system/mcp"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: oven-sh/setup-bun@v1
|
uses: oven-sh/setup-bun@v1
|
||||||
with:
|
with:
|
||||||
bun-version: "1.2.22"
|
bun-version: "1.2.19"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
working-directory: ./app
|
working-directory: ./app
|
||||||
|
|||||||
@@ -32,5 +32,3 @@ packages/media/.env
|
|||||||
docker/tmp
|
docker/tmp
|
||||||
.debug
|
.debug
|
||||||
.history
|
.history
|
||||||
.aider*
|
|
||||||
.vercel
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
[](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-demo?hideExplorer=1&embed=1&view=preview&initialPath=%2Fdata%2Fschema" target="_blank">
|
<a href="https://stackblitz.com/github/bknd-io/bknd-examples?hideExplorer=1&embed=1&view=preview&startScript=example-admin-rich&initialPath=%2Fdata%2Fschema" target="_blank">
|
||||||
<strong>⭐ Live Demo</strong>
|
<strong>⭐ Live Demo</strong>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
@@ -19,7 +19,7 @@ bknd simplifies app development by providing a fully functional backend for data
|
|||||||
**For documentation and examples, please visit https://docs.bknd.io.**
|
**For documentation and examples, please visit https://docs.bknd.io.**
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> This project requires Node.js 22.13 or higher (because of `node:sqlite`).
|
> This project requires Node.js 22 or higher (because of `node:sqlite`).
|
||||||
>
|
>
|
||||||
> Please keep in mind that **bknd** is still under active development
|
> Please keep in mind that **bknd** is still under active development
|
||||||
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
|
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
|
||||||
@@ -27,8 +27,8 @@ bknd simplifies app development by providing a fully functional backend for data
|
|||||||
## Size
|
## Size
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
The size on npm is misleading, as the `bknd` package includes the backend, the ui components as well as the whole backend bundled into the cli including static assets.
|
The size on npm is misleading, as the `bknd` package includes the backend, the ui components as well as the whole backend bundled into the cli including static assets.
|
||||||
|
|
||||||
@@ -52,7 +52,6 @@ Creating digital products always requires developing both the backend (the logic
|
|||||||
* using a React framework (Next.js, React Router, Astro)
|
* using a React framework (Next.js, React Router, Astro)
|
||||||
* 📦 Official API and React SDK with type-safety
|
* 📦 Official API and React SDK with type-safety
|
||||||
* ⚛️ React elements for auto-configured authentication and media components
|
* ⚛️ React elements for auto-configured authentication and media components
|
||||||
* 🛠️ MCP server, client and UI built-in to control your backend
|
|
||||||
|
|
||||||
## Structure
|
## Structure
|
||||||
The package is mainly split into 4 parts, each serving a specific purpose:
|
The package is mainly split into 4 parts, each serving a specific purpose:
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import { afterEach, describe, test, expect, beforeAll, afterAll } from "bun:test";
|
import { afterEach, describe, test, expect } from "bun:test";
|
||||||
import { App, createApp } from "core/test/utils";
|
import { App, createApp } from "core/test/utils";
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import * as proto from "../src/data/prototype";
|
import * as proto from "../src/data/prototype";
|
||||||
import { pick } from "lodash-es";
|
import { pick } from "lodash-es";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
afterEach(async () => (await afterAllCleanup()) && enableConsoleLog());
|
afterEach(afterAllCleanup);
|
||||||
|
|
||||||
describe("App tests", async () => {
|
describe("App tests", async () => {
|
||||||
test("boots and pongs", async () => {
|
test("boots and pongs", async () => {
|
||||||
@@ -22,7 +19,7 @@ describe("App tests", async () => {
|
|||||||
test("plugins", async () => {
|
test("plugins", async () => {
|
||||||
const called: string[] = [];
|
const called: string[] = [];
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
config: {
|
initialConfig: {
|
||||||
auth: {
|
auth: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
|
|||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
hello
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { expect, describe, it, beforeAll, afterAll, mock } from "bun:test";
|
import { expect, describe, it, beforeAll, afterAll } from "bun:test";
|
||||||
import * as adapter from "adapter";
|
import * as adapter from "adapter";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||||
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||||
@@ -9,49 +9,60 @@ beforeAll(disableConsoleLog);
|
|||||||
afterAll(enableConsoleLog);
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
describe("adapter", () => {
|
describe("adapter", () => {
|
||||||
it("makes config", async () => {
|
it("makes config", () => {
|
||||||
expect(omitKeys(await adapter.makeConfig({}), ["connection"])).toEqual({});
|
expect(omitKeys(adapter.makeConfig({}), ["connection"])).toEqual({});
|
||||||
expect(
|
expect(omitKeys(adapter.makeConfig({}, { env: { TEST: "test" } }), ["connection"])).toEqual(
|
||||||
omitKeys(await adapter.makeConfig({}, { env: { TEST: "test" } }), ["connection"]),
|
{},
|
||||||
).toEqual({});
|
);
|
||||||
|
|
||||||
// merges everything returned from `app` with the config
|
// merges everything returned from `app` with the config
|
||||||
expect(
|
expect(
|
||||||
omitKeys(
|
omitKeys(
|
||||||
await adapter.makeConfig(
|
adapter.makeConfig(
|
||||||
{ app: (a) => ({ config: { server: { cors: { origin: a.env.TEST } } } }) },
|
{ app: (a) => ({ initialConfig: { server: { cors: { origin: a.env.TEST } } } }) },
|
||||||
{ env: { TEST: "test" } },
|
{ env: { TEST: "test" } },
|
||||||
),
|
),
|
||||||
["connection"],
|
["connection"],
|
||||||
),
|
),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
config: { server: { cors: { origin: "test" } } },
|
initialConfig: { server: { cors: { origin: "test" } } },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows all properties in app function", async () => {
|
/* it.only("...", async () => {
|
||||||
const called = mock(() => null);
|
const app = await adapter.createAdapterApp();
|
||||||
const config = await adapter.makeConfig(
|
}); */
|
||||||
|
|
||||||
|
it("reuses apps correctly", async () => {
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
|
||||||
|
const first = await adapter.createAdapterApp(
|
||||||
{
|
{
|
||||||
app: (env) => ({
|
initialConfig: { server: { cors: { origin: "random" } } },
|
||||||
connection: { url: "test" },
|
|
||||||
config: { server: { cors: { origin: "test" } } },
|
|
||||||
options: {
|
|
||||||
mode: "db",
|
|
||||||
},
|
|
||||||
onBuilt: () => {
|
|
||||||
called();
|
|
||||||
expect(env).toEqual({ foo: "bar" });
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
{ foo: "bar" },
|
undefined,
|
||||||
|
{ id },
|
||||||
);
|
);
|
||||||
expect(config.connection).toEqual({ url: "test" });
|
const second = await adapter.createAdapterApp();
|
||||||
expect(config.config).toEqual({ server: { cors: { origin: "test" } } });
|
const third = await adapter.createAdapterApp(undefined, undefined, { id });
|
||||||
expect(config.options).toEqual({ mode: "db" });
|
|
||||||
await config.onBuilt?.(null as any);
|
await first.build();
|
||||||
expect(called).toHaveBeenCalled();
|
await second.build();
|
||||||
|
await third.build();
|
||||||
|
|
||||||
|
expect(first.toJSON().server.cors.origin).toEqual("random");
|
||||||
|
expect(first).toBe(third);
|
||||||
|
expect(first).not.toBe(second);
|
||||||
|
expect(second).not.toBe(third);
|
||||||
|
expect(second.toJSON().server.cors.origin).toEqual("*");
|
||||||
|
|
||||||
|
// recreate the first one
|
||||||
|
const first2 = await adapter.createAdapterApp(undefined, undefined, { id, force: true });
|
||||||
|
await first2.build();
|
||||||
|
expect(first2).not.toBe(first);
|
||||||
|
expect(first2).not.toBe(third);
|
||||||
|
expect(first2).not.toBe(second);
|
||||||
|
expect(first2.toJSON().server.cors.origin).toEqual("*");
|
||||||
});
|
});
|
||||||
|
|
||||||
adapterTestSuite(bunTestRunner, {
|
adapterTestSuite(bunTestRunner, {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ describe("Api", async () => {
|
|||||||
expect(api.isAuthVerified()).toBe(false);
|
expect(api.isAuthVerified()).toBe(false);
|
||||||
|
|
||||||
const params = api.getParams();
|
const params = api.getParams();
|
||||||
|
console.log(params);
|
||||||
expect(params.token).toBe(token);
|
expect(params.token).toBe(token);
|
||||||
expect(params.token_transport).toBe("cookie");
|
expect(params.token_transport).toBe("cookie");
|
||||||
expect(params.host).toBe("http://example.com");
|
expect(params.host).toBe("http://example.com");
|
||||||
|
|||||||
@@ -1,23 +1,9 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
|
import { describe, expect, mock, test } from "bun:test";
|
||||||
import type { ModuleBuildContext } from "../../src";
|
import type { ModuleBuildContext } from "../../src";
|
||||||
import { App, createApp } from "core/test/utils";
|
import { App, createApp } from "core/test/utils";
|
||||||
import * as proto from "data/prototype";
|
import * as proto from "../../src/data/prototype";
|
||||||
import { DbModuleManager } from "modules/db/DbModuleManager";
|
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
describe("App", () => {
|
describe("App", () => {
|
||||||
test("use db mode by default", async () => {
|
|
||||||
const app = createApp();
|
|
||||||
await app.build();
|
|
||||||
|
|
||||||
expect(app.mode).toBe("db");
|
|
||||||
expect(app.isReadOnly()).toBe(false);
|
|
||||||
expect(app.modules instanceof DbModuleManager).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("seed includes ctx and app", async () => {
|
test("seed includes ctx and app", async () => {
|
||||||
const called = mock(() => null);
|
const called = mock(() => null);
|
||||||
await createApp({
|
await createApp({
|
||||||
@@ -34,7 +20,6 @@ describe("App", () => {
|
|||||||
"guard",
|
"guard",
|
||||||
"flags",
|
"flags",
|
||||||
"logger",
|
"logger",
|
||||||
"mcp",
|
|
||||||
"helper",
|
"helper",
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
@@ -43,7 +28,7 @@ describe("App", () => {
|
|||||||
expect(called).toHaveBeenCalled();
|
expect(called).toHaveBeenCalled();
|
||||||
|
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
config: {
|
initialConfig: {
|
||||||
data: proto
|
data: proto
|
||||||
.em({
|
.em({
|
||||||
todos: proto.entity("todos", {
|
todos: proto.entity("todos", {
|
||||||
@@ -150,21 +135,4 @@ describe("App", () => {
|
|||||||
// expect async listeners to be executed sync after request
|
// expect async listeners to be executed sync after request
|
||||||
expect(called).toHaveBeenCalled();
|
expect(called).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getMcpClient", async () => {
|
|
||||||
const app = createApp({
|
|
||||||
config: {
|
|
||||||
server: {
|
|
||||||
mcp: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await app.build();
|
|
||||||
const client = app.getMcpClient();
|
|
||||||
const res = await client.listTools();
|
|
||||||
expect(res).toBeDefined();
|
|
||||||
expect(res?.tools.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,11 +13,6 @@ describe("AppServer", () => {
|
|||||||
allow_methods: ["GET", "POST", "PATCH", "PUT", "DELETE"],
|
allow_methods: ["GET", "POST", "PATCH", "PUT", "DELETE"],
|
||||||
allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"],
|
allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"],
|
||||||
},
|
},
|
||||||
mcp: {
|
|
||||||
enabled: false,
|
|
||||||
path: "/api/system/mcp",
|
|
||||||
logLevel: "warning",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,11 +31,6 @@ describe("AppServer", () => {
|
|||||||
allow_methods: ["GET", "POST"],
|
allow_methods: ["GET", "POST"],
|
||||||
allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"],
|
allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"],
|
||||||
},
|
},
|
||||||
mcp: {
|
|
||||||
enabled: false,
|
|
||||||
path: "/api/system/mcp",
|
|
||||||
logLevel: "warning",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
import { describe, expect, mock, test } from "bun:test";
|
|
||||||
import { createApp as internalCreateApp, type CreateAppConfig } from "bknd";
|
|
||||||
import { getDummyConnection } from "../../__test__/helper";
|
|
||||||
import { ModuleManager } from "modules/ModuleManager";
|
|
||||||
import { em, entity, text } from "data/prototype";
|
|
||||||
|
|
||||||
async function createApp(config: CreateAppConfig = {}) {
|
|
||||||
const app = internalCreateApp({
|
|
||||||
connection: getDummyConnection().dummyConnection,
|
|
||||||
...config,
|
|
||||||
options: {
|
|
||||||
...config.options,
|
|
||||||
mode: "code",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await app.build();
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("code-only", () => {
|
|
||||||
test("should create app with correct manager", async () => {
|
|
||||||
const app = await createApp();
|
|
||||||
await app.build();
|
|
||||||
|
|
||||||
expect(app.version()).toBeDefined();
|
|
||||||
expect(app.modules).toBeInstanceOf(ModuleManager);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should not perform database syncs", async () => {
|
|
||||||
const app = await createApp({
|
|
||||||
config: {
|
|
||||||
data: em({
|
|
||||||
test: entity("test", {
|
|
||||||
name: text(),
|
|
||||||
}),
|
|
||||||
}).toJSON(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(app.em.entities.map((e) => e.name)).toEqual(["test"]);
|
|
||||||
expect(
|
|
||||||
await app.em.connection.kysely
|
|
||||||
.selectFrom("sqlite_master")
|
|
||||||
.where("type", "=", "table")
|
|
||||||
.selectAll()
|
|
||||||
.execute(),
|
|
||||||
).toEqual([]);
|
|
||||||
|
|
||||||
// only perform when explicitly forced
|
|
||||||
await app.em.schema().sync({ force: true });
|
|
||||||
expect(
|
|
||||||
await app.em.connection.kysely
|
|
||||||
.selectFrom("sqlite_master")
|
|
||||||
.where("type", "=", "table")
|
|
||||||
.selectAll()
|
|
||||||
.execute()
|
|
||||||
.then((r) => r.map((r) => r.name)),
|
|
||||||
).toEqual(["test", "sqlite_sequence"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should not perform seeding", async () => {
|
|
||||||
const called = mock(() => null);
|
|
||||||
const app = await createApp({
|
|
||||||
config: {
|
|
||||||
data: em({
|
|
||||||
test: entity("test", {
|
|
||||||
name: text(),
|
|
||||||
}),
|
|
||||||
}).toJSON(),
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
seed: async (ctx) => {
|
|
||||||
called();
|
|
||||||
await ctx.em.mutator("test").insertOne({ name: "test" });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await app.em.schema().sync({ force: true });
|
|
||||||
expect(called).not.toHaveBeenCalled();
|
|
||||||
expect(
|
|
||||||
await app.em
|
|
||||||
.repo("test")
|
|
||||||
.findMany({})
|
|
||||||
.then((r) => r.data),
|
|
||||||
).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should sync and perform seeding", async () => {
|
|
||||||
const called = mock(() => null);
|
|
||||||
const app = await createApp({
|
|
||||||
config: {
|
|
||||||
data: em({
|
|
||||||
test: entity("test", {
|
|
||||||
name: text(),
|
|
||||||
}),
|
|
||||||
}).toJSON(),
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
seed: async (ctx) => {
|
|
||||||
called();
|
|
||||||
await ctx.em.mutator("test").insertOne({ name: "test" });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await app.em.schema().sync({ force: true });
|
|
||||||
await app.options?.seed?.({
|
|
||||||
...app.modules.ctx(),
|
|
||||||
app: app,
|
|
||||||
});
|
|
||||||
expect(called).toHaveBeenCalled();
|
|
||||||
expect(
|
|
||||||
await app.em
|
|
||||||
.repo("test")
|
|
||||||
.findMany({})
|
|
||||||
.then((r) => r.data),
|
|
||||||
).toEqual([{ id: 1, name: "test" }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should not allow to modify config", async () => {
|
|
||||||
const app = await createApp();
|
|
||||||
// biome-ignore lint/suspicious/noPrototypeBuiltins: <explanation>
|
|
||||||
expect(app.modules.hasOwnProperty("mutateConfigSafe")).toBe(false);
|
|
||||||
expect(() => {
|
|
||||||
app.modules.configs().auth.enabled = true;
|
|
||||||
}).toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
import { describe, test, expect, beforeEach, beforeAll, afterAll } from "bun:test";
|
|
||||||
import { type App, createApp, createMcpToolCaller } from "core/test/utils";
|
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
|
||||||
import type { McpServer } from "bknd/utils";
|
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* - [x] auth_me
|
|
||||||
* - [x] auth_strategies
|
|
||||||
* - [x] auth_user_create
|
|
||||||
* - [x] auth_user_token
|
|
||||||
* - [x] auth_user_password_change
|
|
||||||
* - [x] auth_user_password_test
|
|
||||||
* - [x] config_auth_get
|
|
||||||
* - [x] config_auth_update
|
|
||||||
* - [x] config_auth_strategies_get
|
|
||||||
* - [x] config_auth_strategies_add
|
|
||||||
* - [x] config_auth_strategies_update
|
|
||||||
* - [x] config_auth_strategies_remove
|
|
||||||
* - [x] config_auth_roles_get
|
|
||||||
* - [x] config_auth_roles_add
|
|
||||||
* - [x] config_auth_roles_update
|
|
||||||
* - [x] config_auth_roles_remove
|
|
||||||
*/
|
|
||||||
describe("mcp auth", async () => {
|
|
||||||
let app: App;
|
|
||||||
let server: McpServer;
|
|
||||||
beforeEach(async () => {
|
|
||||||
app = createApp({
|
|
||||||
config: {
|
|
||||||
auth: {
|
|
||||||
enabled: true,
|
|
||||||
jwt: {
|
|
||||||
secret: "secret",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
mcp: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await app.build();
|
|
||||||
await app.getMcpClient().ping();
|
|
||||||
server = app.mcp!;
|
|
||||||
server.setLogLevel("error");
|
|
||||||
server.onNotification((message) => {
|
|
||||||
console.dir(message, { depth: null });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const tool = createMcpToolCaller();
|
|
||||||
|
|
||||||
test("auth_*", async () => {
|
|
||||||
const me = await tool(server, "auth_me", {});
|
|
||||||
expect(me.user).toBeNull();
|
|
||||||
|
|
||||||
// strategies
|
|
||||||
const strategies = await tool(server, "auth_strategies", {});
|
|
||||||
expect(Object.keys(strategies.strategies).length).toEqual(1);
|
|
||||||
expect(strategies.strategies.password.enabled).toBe(true);
|
|
||||||
|
|
||||||
// create user
|
|
||||||
const user = await tool(
|
|
||||||
server,
|
|
||||||
"auth_user_create",
|
|
||||||
{
|
|
||||||
email: "test@test.com",
|
|
||||||
password: "12345678",
|
|
||||||
},
|
|
||||||
new Headers(),
|
|
||||||
);
|
|
||||||
expect(user.email).toBe("test@test.com");
|
|
||||||
|
|
||||||
// create token
|
|
||||||
const token = await tool(
|
|
||||||
server,
|
|
||||||
"auth_user_token",
|
|
||||||
{
|
|
||||||
email: "test@test.com",
|
|
||||||
},
|
|
||||||
new Headers(),
|
|
||||||
);
|
|
||||||
expect(token.token).toBeDefined();
|
|
||||||
expect(token.user.email).toBe("test@test.com");
|
|
||||||
|
|
||||||
// me
|
|
||||||
const me2 = await tool(
|
|
||||||
server,
|
|
||||||
"auth_me",
|
|
||||||
{},
|
|
||||||
new Request("http://localhost", {
|
|
||||||
headers: new Headers({
|
|
||||||
Authorization: `Bearer ${token.token}`,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(me2.user.email).toBe("test@test.com");
|
|
||||||
|
|
||||||
// change password
|
|
||||||
const changePassword = await tool(
|
|
||||||
server,
|
|
||||||
"auth_user_password_change",
|
|
||||||
{
|
|
||||||
email: "test@test.com",
|
|
||||||
password: "87654321",
|
|
||||||
},
|
|
||||||
new Headers(),
|
|
||||||
);
|
|
||||||
expect(changePassword.changed).toBe(true);
|
|
||||||
|
|
||||||
// test password
|
|
||||||
const testPassword = await tool(
|
|
||||||
server,
|
|
||||||
"auth_user_password_test",
|
|
||||||
{
|
|
||||||
email: "test@test.com",
|
|
||||||
password: "87654321",
|
|
||||||
},
|
|
||||||
new Headers(),
|
|
||||||
);
|
|
||||||
expect(testPassword.valid).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("config_auth_{get,update}", async () => {
|
|
||||||
expect(await tool(server, "config_auth_get", {})).toEqual({
|
|
||||||
path: "",
|
|
||||||
secrets: false,
|
|
||||||
partial: false,
|
|
||||||
value: app.toJSON().auth,
|
|
||||||
});
|
|
||||||
|
|
||||||
// update
|
|
||||||
await tool(server, "config_auth_update", {
|
|
||||||
value: {
|
|
||||||
allow_register: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(app.toJSON().auth.allow_register).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("config_auth_strategies_{get,add,update,remove}", async () => {
|
|
||||||
const strategies = await tool(server, "config_auth_strategies_get", {
|
|
||||||
key: "password",
|
|
||||||
});
|
|
||||||
expect(strategies).toEqual({
|
|
||||||
secrets: false,
|
|
||||||
module: "auth",
|
|
||||||
key: "password",
|
|
||||||
value: {
|
|
||||||
enabled: true,
|
|
||||||
type: "password",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// add google oauth
|
|
||||||
const addGoogleOauth = await tool(server, "config_auth_strategies_add", {
|
|
||||||
key: "google",
|
|
||||||
value: {
|
|
||||||
type: "oauth",
|
|
||||||
enabled: true,
|
|
||||||
config: {
|
|
||||||
name: "google",
|
|
||||||
type: "oidc",
|
|
||||||
client: {
|
|
||||||
client_id: "client_id",
|
|
||||||
client_secret: "client_secret",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
return_config: true,
|
|
||||||
});
|
|
||||||
expect(addGoogleOauth.config.google.enabled).toBe(true);
|
|
||||||
expect(app.toJSON().auth.strategies.google?.enabled).toBe(true);
|
|
||||||
|
|
||||||
// update (disable) google oauth
|
|
||||||
await tool(server, "config_auth_strategies_update", {
|
|
||||||
key: "google",
|
|
||||||
value: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(app.toJSON().auth.strategies.google?.enabled).toBe(false);
|
|
||||||
|
|
||||||
// remove google oauth
|
|
||||||
await tool(server, "config_auth_strategies_remove", {
|
|
||||||
key: "google",
|
|
||||||
});
|
|
||||||
expect(app.toJSON().auth.strategies.google).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("config_auth_roles_{get,add,update,remove}", async () => {
|
|
||||||
// add role
|
|
||||||
const addGuestRole = await tool(server, "config_auth_roles_add", {
|
|
||||||
key: "guest",
|
|
||||||
value: {
|
|
||||||
permissions: ["read", "write"],
|
|
||||||
},
|
|
||||||
return_config: true,
|
|
||||||
});
|
|
||||||
expect(addGuestRole.config.guest.permissions).toEqual(["read", "write"]);
|
|
||||||
|
|
||||||
// update role
|
|
||||||
await tool(server, "config_auth_roles_update", {
|
|
||||||
key: "guest",
|
|
||||||
value: {
|
|
||||||
permissions: ["read"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(app.toJSON().auth.roles?.guest?.permissions).toEqual(["read"]);
|
|
||||||
|
|
||||||
// get role
|
|
||||||
const getGuestRole = await tool(server, "config_auth_roles_get", {
|
|
||||||
key: "guest",
|
|
||||||
});
|
|
||||||
expect(getGuestRole.value.permissions).toEqual(["read"]);
|
|
||||||
|
|
||||||
// remove role
|
|
||||||
await tool(server, "config_auth_roles_remove", {
|
|
||||||
key: "guest",
|
|
||||||
});
|
|
||||||
expect(app.toJSON().auth.roles?.guest).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
|
|
||||||
import { createApp } from "core/test/utils";
|
|
||||||
import { registries } from "index";
|
|
||||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
describe("mcp", () => {
|
|
||||||
it("should have tools", async () => {
|
|
||||||
registries.media.register("local", StorageLocalAdapter);
|
|
||||||
|
|
||||||
const app = createApp({
|
|
||||||
config: {
|
|
||||||
auth: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
media: {
|
|
||||||
enabled: true,
|
|
||||||
adapter: {
|
|
||||||
type: "local",
|
|
||||||
config: {
|
|
||||||
path: "./",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
mcp: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await app.build();
|
|
||||||
|
|
||||||
// expect mcp to not be loaded yet
|
|
||||||
expect(app.mcp).toBeNull();
|
|
||||||
|
|
||||||
// after first request, mcp should be loaded
|
|
||||||
await app.getMcpClient().listTools();
|
|
||||||
expect(app.mcp?.tools.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,347 +0,0 @@
|
|||||||
import { describe, test, expect, beforeEach, beforeAll, afterAll } from "bun:test";
|
|
||||||
import { type App, createApp, createMcpToolCaller } from "core/test/utils";
|
|
||||||
import { getSystemMcp } from "modules/mcp/system-mcp";
|
|
||||||
import { pickKeys, type McpServer } from "bknd/utils";
|
|
||||||
import { entity, text } from "bknd";
|
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* - [ ] data_sync
|
|
||||||
* - [x] data_entity_fn_count
|
|
||||||
* - [x] data_entity_fn_exists
|
|
||||||
* - [x] data_entity_read_one
|
|
||||||
* - [x] data_entity_read_many
|
|
||||||
* - [x] data_entity_insert
|
|
||||||
* - [x] data_entity_update_many
|
|
||||||
* - [x] data_entity_update_one
|
|
||||||
* - [x] data_entity_delete_one
|
|
||||||
* - [x] data_entity_delete_many
|
|
||||||
* - [x] data_entity_info
|
|
||||||
* - [ ] config_data_get
|
|
||||||
* - [ ] config_data_update
|
|
||||||
* - [x] config_data_entities_get
|
|
||||||
* - [x] config_data_entities_add
|
|
||||||
* - [x] config_data_entities_update
|
|
||||||
* - [x] config_data_entities_remove
|
|
||||||
* - [x] config_data_relations_add
|
|
||||||
* - [x] config_data_relations_get
|
|
||||||
* - [x] config_data_relations_update
|
|
||||||
* - [x] config_data_relations_remove
|
|
||||||
* - [x] config_data_indices_get
|
|
||||||
* - [x] config_data_indices_add
|
|
||||||
* - [x] config_data_indices_update
|
|
||||||
* - [x] config_data_indices_remove
|
|
||||||
*/
|
|
||||||
describe("mcp data", async () => {
|
|
||||||
let app: App;
|
|
||||||
let server: McpServer;
|
|
||||||
beforeEach(async () => {
|
|
||||||
const time = performance.now();
|
|
||||||
app = createApp({
|
|
||||||
config: {
|
|
||||||
server: {
|
|
||||||
mcp: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await app.build();
|
|
||||||
await app.getMcpClient().ping();
|
|
||||||
server = app.mcp!;
|
|
||||||
server.setLogLevel("error");
|
|
||||||
server.onNotification((message) => {
|
|
||||||
console.dir(message, { depth: null });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const tool = createMcpToolCaller();
|
|
||||||
|
|
||||||
test("config_data_entities_{add,get,update,remove}", async () => {
|
|
||||||
const result = await tool(server, "config_data_entities_add", {
|
|
||||||
key: "test",
|
|
||||||
return_config: true,
|
|
||||||
value: {},
|
|
||||||
});
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
expect(result.module).toBe("data");
|
|
||||||
expect(result.config.test?.type).toEqual("regular");
|
|
||||||
|
|
||||||
const entities = Object.keys(app.toJSON().data.entities ?? {});
|
|
||||||
expect(entities).toContain("test");
|
|
||||||
|
|
||||||
{
|
|
||||||
// get
|
|
||||||
const result = await tool(server, "config_data_entities_get", {
|
|
||||||
key: "test",
|
|
||||||
});
|
|
||||||
expect(result.module).toBe("data");
|
|
||||||
expect(result.key).toBe("test");
|
|
||||||
expect(result.value.type).toEqual("regular");
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// update
|
|
||||||
const result = await tool(server, "config_data_entities_update", {
|
|
||||||
key: "test",
|
|
||||||
return_config: true,
|
|
||||||
value: {
|
|
||||||
config: {
|
|
||||||
name: "Test",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
expect(result.module).toBe("data");
|
|
||||||
expect(result.config.test.config?.name).toEqual("Test");
|
|
||||||
expect(app.toJSON().data.entities?.test?.config?.name).toEqual("Test");
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// remove
|
|
||||||
const result = await tool(server, "config_data_entities_remove", {
|
|
||||||
key: "test",
|
|
||||||
});
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
expect(result.module).toBe("data");
|
|
||||||
expect(app.toJSON().data.entities?.test).toBeUndefined();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("config_data_relations_{add,get,update,remove}", async () => {
|
|
||||||
// create posts and comments
|
|
||||||
await tool(server, "config_data_entities_add", {
|
|
||||||
key: "posts",
|
|
||||||
value: {},
|
|
||||||
});
|
|
||||||
await tool(server, "config_data_entities_add", {
|
|
||||||
key: "comments",
|
|
||||||
value: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(Object.keys(app.toJSON().data.entities ?? {})).toEqual(["posts", "comments"]);
|
|
||||||
|
|
||||||
// create relation
|
|
||||||
await tool(server, "config_data_relations_add", {
|
|
||||||
key: "", // doesn't matter
|
|
||||||
value: {
|
|
||||||
type: "n:1",
|
|
||||||
source: "comments",
|
|
||||||
target: "posts",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const config = app.toJSON().data;
|
|
||||||
expect(
|
|
||||||
pickKeys((config.relations?.n1_comments_posts as any) ?? {}, ["type", "source", "target"]),
|
|
||||||
).toEqual({
|
|
||||||
type: "n:1",
|
|
||||||
source: "comments",
|
|
||||||
target: "posts",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(config.entities?.comments?.fields?.posts_id?.type).toBe("relation");
|
|
||||||
|
|
||||||
{
|
|
||||||
// info
|
|
||||||
const postsInfo = await tool(server, "data_entity_info", {
|
|
||||||
entity: "posts",
|
|
||||||
});
|
|
||||||
expect(postsInfo.fields).toEqual(["id"]);
|
|
||||||
expect(postsInfo.relations.all.length).toBe(1);
|
|
||||||
|
|
||||||
const commentsInfo = await tool(server, "data_entity_info", {
|
|
||||||
entity: "comments",
|
|
||||||
});
|
|
||||||
expect(commentsInfo.fields).toEqual(["id", "posts_id"]);
|
|
||||||
expect(commentsInfo.relations.all.length).toBe(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// update
|
|
||||||
await tool(server, "config_data_relations_update", {
|
|
||||||
key: "n1_comments_posts",
|
|
||||||
value: {
|
|
||||||
config: {
|
|
||||||
with_limit: 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect((app.toJSON().data.relations?.n1_comments_posts?.config as any)?.with_limit).toBe(10);
|
|
||||||
|
|
||||||
// delete
|
|
||||||
await tool(server, "config_data_relations_remove", {
|
|
||||||
key: "n1_comments_posts",
|
|
||||||
});
|
|
||||||
expect(app.toJSON().data.relations?.n1_comments_posts).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("config_data_indices_update", async () => {
|
|
||||||
expect(server.tools.map((t) => t.name).includes("config_data_indices_update")).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("config_data_indices_{add,get,remove}", async () => {
|
|
||||||
// create posts and comments
|
|
||||||
await tool(server, "config_data_entities_add", {
|
|
||||||
key: "posts",
|
|
||||||
value: entity("posts", {
|
|
||||||
title: text(),
|
|
||||||
content: text(),
|
|
||||||
}).toJSON(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// add index on title
|
|
||||||
await tool(server, "config_data_indices_add", {
|
|
||||||
key: "", // auto generated
|
|
||||||
value: {
|
|
||||||
entity: "posts",
|
|
||||||
fields: ["title"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(app.toJSON().data.indices?.idx_posts_title).toEqual({
|
|
||||||
entity: "posts",
|
|
||||||
fields: ["title"],
|
|
||||||
unique: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// delete
|
|
||||||
await tool(server, "config_data_indices_remove", {
|
|
||||||
key: "idx_posts_title",
|
|
||||||
});
|
|
||||||
expect(app.toJSON().data.indices?.idx_posts_title).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("data_entity_*", async () => {
|
|
||||||
// create posts and comments
|
|
||||||
await tool(server, "config_data_entities_add", {
|
|
||||||
key: "posts",
|
|
||||||
value: entity("posts", {
|
|
||||||
title: text(),
|
|
||||||
content: text(),
|
|
||||||
}).toJSON(),
|
|
||||||
});
|
|
||||||
await tool(server, "config_data_entities_add", {
|
|
||||||
key: "comments",
|
|
||||||
value: entity("comments", {
|
|
||||||
content: text(),
|
|
||||||
}).toJSON(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// insert a few posts
|
|
||||||
for (let i = 0; i < 10; i++) {
|
|
||||||
await tool(server, "data_entity_insert", {
|
|
||||||
entity: "posts",
|
|
||||||
json: {
|
|
||||||
title: `Post ${i}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// insert a few comments
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
await tool(server, "data_entity_insert", {
|
|
||||||
entity: "comments",
|
|
||||||
json: {
|
|
||||||
content: `Comment ${i}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await tool(server, "data_entity_read_many", {
|
|
||||||
entity: "posts",
|
|
||||||
limit: 5,
|
|
||||||
});
|
|
||||||
expect(result.data.length).toBe(5);
|
|
||||||
expect(result.meta.items).toBe(5);
|
|
||||||
expect(result.meta.total).toBe(10);
|
|
||||||
expect(result.data[0].title).toBe("Post 0");
|
|
||||||
|
|
||||||
{
|
|
||||||
// count
|
|
||||||
const result = await tool(server, "data_entity_fn_count", {
|
|
||||||
entity: "posts",
|
|
||||||
});
|
|
||||||
expect(result.count).toBe(10);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// exists
|
|
||||||
const res = await tool(server, "data_entity_fn_exists", {
|
|
||||||
entity: "posts",
|
|
||||||
json: {
|
|
||||||
id: result.data[0].id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(res.exists).toBe(true);
|
|
||||||
|
|
||||||
const res2 = await tool(server, "data_entity_fn_exists", {
|
|
||||||
entity: "posts",
|
|
||||||
json: {
|
|
||||||
id: "123",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(res2.exists).toBe(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// update
|
|
||||||
await tool(server, "data_entity_update_one", {
|
|
||||||
entity: "posts",
|
|
||||||
id: result.data[0].id,
|
|
||||||
json: {
|
|
||||||
title: "Post 0 updated",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const result2 = await tool(server, "data_entity_read_one", {
|
|
||||||
entity: "posts",
|
|
||||||
id: result.data[0].id,
|
|
||||||
});
|
|
||||||
expect(result2.data.title).toBe("Post 0 updated");
|
|
||||||
|
|
||||||
// delete the second post
|
|
||||||
await tool(server, "data_entity_delete_one", {
|
|
||||||
entity: "posts",
|
|
||||||
id: result.data[1].id,
|
|
||||||
});
|
|
||||||
const result3 = await tool(server, "data_entity_read_many", {
|
|
||||||
entity: "posts",
|
|
||||||
limit: 2,
|
|
||||||
});
|
|
||||||
expect(result3.data.map((p) => p.id)).toEqual([1, 3]);
|
|
||||||
|
|
||||||
// update many
|
|
||||||
await tool(server, "data_entity_update_many", {
|
|
||||||
entity: "posts",
|
|
||||||
update: {
|
|
||||||
title: "Post updated",
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
title: { $isnull: 0 },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const result4 = await tool(server, "data_entity_read_many", {
|
|
||||||
entity: "posts",
|
|
||||||
limit: 10,
|
|
||||||
});
|
|
||||||
expect(result4.data.length).toBe(9);
|
|
||||||
expect(result4.data.map((p) => p.title)).toEqual(
|
|
||||||
Array.from({ length: 9 }, () => "Post updated"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// delete many
|
|
||||||
await tool(server, "data_entity_delete_many", {
|
|
||||||
entity: "posts",
|
|
||||||
json: {
|
|
||||||
title: { $isnull: 0 },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const result5 = await tool(server, "data_entity_read_many", {
|
|
||||||
entity: "posts",
|
|
||||||
limit: 10,
|
|
||||||
});
|
|
||||||
expect(result5.data.length).toBe(0);
|
|
||||||
expect(result5.meta.items).toBe(0);
|
|
||||||
expect(result5.meta.total).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
import { describe, test, expect, beforeAll, beforeEach, afterAll } from "bun:test";
|
|
||||||
import { type App, createApp, createMcpToolCaller } from "core/test/utils";
|
|
||||||
import { getSystemMcp } from "modules/mcp/system-mcp";
|
|
||||||
import { registries } from "index";
|
|
||||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
|
||||||
import type { McpServer } from "bknd/utils";
|
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* - [x] config_media_get
|
|
||||||
* - [x] config_media_update
|
|
||||||
* - [x] config_media_adapter_get
|
|
||||||
* - [x] config_media_adapter_update
|
|
||||||
*/
|
|
||||||
describe("mcp media", async () => {
|
|
||||||
let app: App;
|
|
||||||
let server: McpServer;
|
|
||||||
beforeEach(async () => {
|
|
||||||
registries.media.register("local", StorageLocalAdapter);
|
|
||||||
app = createApp({
|
|
||||||
config: {
|
|
||||||
media: {
|
|
||||||
enabled: true,
|
|
||||||
adapter: {
|
|
||||||
type: "local",
|
|
||||||
config: {
|
|
||||||
path: "./",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
mcp: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await app.build();
|
|
||||||
await app.getMcpClient().ping();
|
|
||||||
server = app.mcp!;
|
|
||||||
server.setLogLevel("error");
|
|
||||||
server.onNotification((message) => {
|
|
||||||
console.dir(message, { depth: null });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const tool = createMcpToolCaller();
|
|
||||||
|
|
||||||
test("config_media_{get,update}", async () => {
|
|
||||||
const result = await tool(server, "config_media_get", {});
|
|
||||||
expect(result).toEqual({
|
|
||||||
path: "",
|
|
||||||
secrets: false,
|
|
||||||
partial: false,
|
|
||||||
value: app.toJSON().media,
|
|
||||||
});
|
|
||||||
|
|
||||||
// partial
|
|
||||||
expect((await tool(server, "config_media_get", { path: "adapter" })).value).toEqual({
|
|
||||||
type: "local",
|
|
||||||
config: {
|
|
||||||
path: "./",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// update
|
|
||||||
await tool(server, "config_media_update", {
|
|
||||||
value: {
|
|
||||||
storage: {
|
|
||||||
body_max_size: 1024 * 1024 * 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
return_config: true,
|
|
||||||
});
|
|
||||||
expect(app.toJSON().media.storage.body_max_size).toBe(1024 * 1024 * 10);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("config_media_adapter_{get,update}", async () => {
|
|
||||||
const result = await tool(server, "config_media_adapter_get", {});
|
|
||||||
expect(result).toEqual({
|
|
||||||
secrets: false,
|
|
||||||
value: app.toJSON().media.adapter,
|
|
||||||
});
|
|
||||||
|
|
||||||
// update
|
|
||||||
await tool(server, "config_media_adapter_update", {
|
|
||||||
value: {
|
|
||||||
type: "local",
|
|
||||||
config: {
|
|
||||||
path: "./subdir",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const adapter = app.toJSON().media.adapter as any;
|
|
||||||
expect(adapter.config.path).toBe("./subdir");
|
|
||||||
expect(adapter.type).toBe("local");
|
|
||||||
|
|
||||||
// set to s3
|
|
||||||
{
|
|
||||||
await tool(server, "config_media_adapter_update", {
|
|
||||||
value: {
|
|
||||||
type: "s3",
|
|
||||||
config: {
|
|
||||||
access_key: "123",
|
|
||||||
secret_access_key: "456",
|
|
||||||
url: "https://example.com/what",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const adapter = app.toJSON(true).media.adapter as any;
|
|
||||||
expect(adapter.type).toBe("s3");
|
|
||||||
expect(adapter.config.url).toBe("https://example.com/what");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
||||||
import { type App, createApp, createMcpToolCaller } from "core/test/utils";
|
|
||||||
import type { McpServer } from "bknd/utils";
|
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* - [x] config_server_get
|
|
||||||
* - [x] config_server_update
|
|
||||||
*/
|
|
||||||
describe("mcp system", async () => {
|
|
||||||
let app: App;
|
|
||||||
let server: McpServer;
|
|
||||||
beforeAll(async () => {
|
|
||||||
app = createApp({
|
|
||||||
config: {
|
|
||||||
server: {
|
|
||||||
mcp: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await app.build();
|
|
||||||
await app.getMcpClient().ping();
|
|
||||||
server = app.mcp!;
|
|
||||||
});
|
|
||||||
|
|
||||||
const tool = createMcpToolCaller();
|
|
||||||
|
|
||||||
test("config_server_get", async () => {
|
|
||||||
const result = await tool(server, "config_server_get", {});
|
|
||||||
expect(JSON.parse(JSON.stringify(result))).toEqual({
|
|
||||||
path: "",
|
|
||||||
secrets: false,
|
|
||||||
partial: false,
|
|
||||||
value: JSON.parse(JSON.stringify(app.toJSON().server)),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("config_server_get2", async () => {
|
|
||||||
const result = await tool(server, "config_server_get", {});
|
|
||||||
expect(JSON.parse(JSON.stringify(result))).toEqual({
|
|
||||||
path: "",
|
|
||||||
secrets: false,
|
|
||||||
partial: false,
|
|
||||||
value: JSON.parse(JSON.stringify(app.toJSON().server)),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("config_server_update", async () => {
|
|
||||||
const original = JSON.parse(JSON.stringify(app.toJSON().server));
|
|
||||||
const result = await tool(server, "config_server_update", {
|
|
||||||
value: {
|
|
||||||
cors: {
|
|
||||||
origin: "http://localhost",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
return_config: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(JSON.parse(JSON.stringify(result))).toEqual({
|
|
||||||
success: true,
|
|
||||||
module: "server",
|
|
||||||
config: {
|
|
||||||
...original,
|
|
||||||
cors: {
|
|
||||||
...original.cors,
|
|
||||||
origin: "http://localhost",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(app.toJSON().server.cors.origin).toBe("http://localhost");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import { AppEvents } from "App";
|
|
||||||
import { describe, test, expect, beforeAll, mock } from "bun:test";
|
|
||||||
import { type App, createApp, createMcpToolCaller } from "core/test/utils";
|
|
||||||
import type { McpServer } from "bknd/utils";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* - [x] system_config
|
|
||||||
* - [x] system_build
|
|
||||||
* - [x] system_ping
|
|
||||||
* - [x] system_info
|
|
||||||
*/
|
|
||||||
describe("mcp system", async () => {
|
|
||||||
let app: App;
|
|
||||||
let server: McpServer;
|
|
||||||
beforeAll(async () => {
|
|
||||||
app = createApp({
|
|
||||||
config: {
|
|
||||||
server: {
|
|
||||||
mcp: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await app.build();
|
|
||||||
await app.getMcpClient().ping();
|
|
||||||
server = app.mcp!;
|
|
||||||
});
|
|
||||||
|
|
||||||
const tool = createMcpToolCaller();
|
|
||||||
|
|
||||||
test("system_ping", async () => {
|
|
||||||
const result = await tool(server, "system_ping", {});
|
|
||||||
expect(result).toEqual({ pong: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
test("system_info", async () => {
|
|
||||||
const result = await tool(server, "system_info", {});
|
|
||||||
expect(Object.keys(result).length).toBeGreaterThan(0);
|
|
||||||
expect(Object.keys(result)).toContainValues(["version", "runtime", "connection"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("system_build", async () => {
|
|
||||||
const called = mock(() => null);
|
|
||||||
|
|
||||||
app.emgr.onEvent(AppEvents.AppBuiltEvent, () => void called(), { once: true });
|
|
||||||
|
|
||||||
const result = await tool(server, "system_build", {});
|
|
||||||
expect(called).toHaveBeenCalledTimes(1);
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("system_config", async () => {
|
|
||||||
const result = await tool(server, "system_config", {});
|
|
||||||
expect(result).toEqual(app.toJSON());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import { describe, it, expect, mock, beforeAll, afterAll } from "bun:test";
|
|
||||||
import { createApp } from "core/test/utils";
|
|
||||||
import { syncConfig } from "plugins/dev/sync-config.plugin";
|
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
describe("syncConfig", () => {
|
|
||||||
it("should only sync if enabled", async () => {
|
|
||||||
const called = mock(() => null);
|
|
||||||
const app = createApp();
|
|
||||||
await app.build();
|
|
||||||
|
|
||||||
await syncConfig({
|
|
||||||
write: () => {
|
|
||||||
called();
|
|
||||||
},
|
|
||||||
enabled: false,
|
|
||||||
includeFirstBoot: false,
|
|
||||||
})(app).onBuilt?.();
|
|
||||||
expect(called).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
await syncConfig({
|
|
||||||
write: () => {
|
|
||||||
called();
|
|
||||||
},
|
|
||||||
enabled: false,
|
|
||||||
includeFirstBoot: true,
|
|
||||||
})(app).onBuilt?.();
|
|
||||||
expect(called).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
await syncConfig({
|
|
||||||
write: () => {
|
|
||||||
called();
|
|
||||||
},
|
|
||||||
enabled: true,
|
|
||||||
includeFirstBoot: true,
|
|
||||||
})(app).onBuilt?.();
|
|
||||||
expect(called).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should respect secrets", async () => {
|
|
||||||
const called = mock(() => null);
|
|
||||||
const app = createApp({
|
|
||||||
config: {
|
|
||||||
auth: {
|
|
||||||
enabled: true,
|
|
||||||
jwt: {
|
|
||||||
secret: "test",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await app.build();
|
|
||||||
|
|
||||||
await syncConfig({
|
|
||||||
write: async (config) => {
|
|
||||||
called();
|
|
||||||
expect(config.auth.jwt.secret).toBe("test");
|
|
||||||
},
|
|
||||||
enabled: true,
|
|
||||||
includeSecrets: true,
|
|
||||||
includeFirstBoot: true,
|
|
||||||
})(app).onBuilt?.();
|
|
||||||
|
|
||||||
await syncConfig({
|
|
||||||
write: async (config) => {
|
|
||||||
called();
|
|
||||||
// it's an important test, because the `jwt` part is omitted if secrets=false in general app.toJSON()
|
|
||||||
// but it's required to get the app running
|
|
||||||
expect(config.auth.jwt.secret).toBe("");
|
|
||||||
},
|
|
||||||
enabled: true,
|
|
||||||
includeSecrets: false,
|
|
||||||
includeFirstBoot: true,
|
|
||||||
})(app).onBuilt?.();
|
|
||||||
expect(called).toHaveBeenCalledTimes(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,12 +1,8 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { registries } from "../../src";
|
import { registries } from "../../src";
|
||||||
import { createApp } from "core/test/utils";
|
import { createApp } from "core/test/utils";
|
||||||
import * as proto from "../../src/data/prototype";
|
import * as proto from "../../src/data/prototype";
|
||||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
describe("repros", async () => {
|
describe("repros", async () => {
|
||||||
/**
|
/**
|
||||||
@@ -92,7 +88,7 @@ describe("repros", async () => {
|
|||||||
fns.relation(schema.product_likes).manyToOne(schema.users);
|
fns.relation(schema.product_likes).manyToOne(schema.users);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const app = createApp({ config: { data: schema.toJSON() } });
|
const app = createApp({ initialConfig: { data: schema.toJSON() } });
|
||||||
await app.build();
|
await app.build();
|
||||||
|
|
||||||
const info = (await (await app.server.request("/api/data/info/products")).json()) as any;
|
const info = (await (await app.server.request("/api/data/info/products")).json()) as any;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
|
||||||
import { Event, EventManager, InvalidEventReturn, NoParamEvent } from "../../src/core/events";
|
import { Event, EventManager, InvalidEventReturn, NoParamEvent } from "../../src/core/events";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
import { disableConsoleLog, enableConsoleLog } from "../helper";
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
beforeAll(disableConsoleLog);
|
||||||
afterAll(enableConsoleLog);
|
afterAll(enableConsoleLog);
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ describe("Core Utils", async () => {
|
|||||||
expect(utils.getContentName(request)).toBe(name);
|
expect(utils.getContentName(request)).toBe(name);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("detectImageDimensions", async () => {
|
test.only("detectImageDimensions", async () => {
|
||||||
// wrong
|
// wrong
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
expect(utils.detectImageDimensions(new ArrayBuffer(), "text/plain")).rejects.toThrow();
|
expect(utils.detectImageDimensions(new ArrayBuffer(), "text/plain")).rejects.toThrow();
|
||||||
@@ -267,12 +267,12 @@ describe("Core Utils", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("dates", () => {
|
describe("dates", () => {
|
||||||
test("formats local time", () => {
|
test.only("formats local time", () => {
|
||||||
expect(utils.datetimeStringUTC("2025-02-21T16:48:25.841Z")).toBe("2025-02-21 16:48:25");
|
expect(utils.datetimeStringUTC("2025-02-21T16:48:25.841Z")).toBe("2025-02-21 16:48:25");
|
||||||
/*console.log(utils.datetimeStringUTC(new Date()));
|
console.log(utils.datetimeStringUTC(new Date()));
|
||||||
console.log(utils.datetimeStringUTC());
|
console.log(utils.datetimeStringUTC());
|
||||||
console.log(new Date());
|
console.log(new Date());
|
||||||
console.log("timezone", Intl.DateTimeFormat().resolvedOptions().timeZone); */
|
console.log("timezone", Intl.DateTimeFormat().resolvedOptions().timeZone);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import { parse } from "core/utils/schema";
|
|||||||
|
|
||||||
import { DataController } from "../../src/data/api/DataController";
|
import { DataController } from "../../src/data/api/DataController";
|
||||||
import { dataConfigSchema } from "../../src/data/data-schema";
|
import { dataConfigSchema } from "../../src/data/data-schema";
|
||||||
import { getDummyConnection } from "../helper";
|
import { disableConsoleLog, enableConsoleLog, getDummyConnection } from "../helper";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
import type { RepositoryResultJSON } from "data/entities/query/RepositoryResult";
|
import type { RepositoryResultJSON } from "data/entities/query/RepositoryResult";
|
||||||
import type { MutatorResultJSON } from "data/entities/mutation/MutatorResult";
|
import type { MutatorResultJSON } from "data/entities/mutation/MutatorResult";
|
||||||
import { Entity, EntityManager, type EntityData } from "data/entities";
|
import { Entity, EntityManager, type EntityData } from "data/entities";
|
||||||
@@ -14,7 +13,7 @@ import { TextField } from "data/fields";
|
|||||||
import { ManyToOneRelation } from "data/relations";
|
import { ManyToOneRelation } from "data/relations";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
beforeAll(() => disableConsoleLog());
|
beforeAll(() => disableConsoleLog(["log", "warn"]));
|
||||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||||
|
|
||||||
const dataConfig = parse(dataConfigSchema, {});
|
const dataConfig = parse(dataConfigSchema, {});
|
||||||
|
|||||||
@@ -47,4 +47,8 @@ describe("[data] Entity", async () => {
|
|||||||
entity.addField(field);
|
entity.addField(field);
|
||||||
expect(entity.getField("new_field")).toBe(field);
|
expect(entity.getField("new_field")).toBe(field);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.only("types", async () => {
|
||||||
|
console.log(entity.toTypes());
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { Entity, EntityManager } from "data/entities";
|
import { Entity, EntityManager } from "data/entities";
|
||||||
import { ManyToOneRelation } from "data/relations";
|
import { ManyToOneRelation } from "data/relations";
|
||||||
import { TextField } from "data/fields";
|
import { TextField } from "data/fields";
|
||||||
import { JoinBuilder } from "data/entities/query/JoinBuilder";
|
import { JoinBuilder } from "data/entities/query/JoinBuilder";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
afterAll(afterAllCleanup);
|
||||||
|
|
||||||
describe("[data] JoinBuilder", async () => {
|
describe("[data] JoinBuilder", async () => {
|
||||||
test("missing relation", async () => {
|
test("missing relation", async () => {
|
||||||
|
|||||||
@@ -9,14 +9,13 @@ import {
|
|||||||
} from "data/relations";
|
} from "data/relations";
|
||||||
import { NumberField, TextField } from "data/fields";
|
import { NumberField, TextField } from "data/fields";
|
||||||
import * as proto from "data/prototype";
|
import * as proto from "data/prototype";
|
||||||
import { getDummyConnection } from "../../helper";
|
import { getDummyConnection, disableConsoleLog, enableConsoleLog } from "../../helper";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
import { MutatorEvents } from "data/events";
|
import { MutatorEvents } from "data/events";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
afterAll(afterAllCleanup);
|
afterAll(afterAllCleanup);
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
beforeAll(() => disableConsoleLog(["log", "warn"]));
|
||||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||||
|
|
||||||
describe("[data] Mutator (base)", async () => {
|
describe("[data] Mutator (base)", async () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import type { Kysely, Transaction } from "kysely";
|
import type { Kysely, Transaction } from "kysely";
|
||||||
import { TextField } from "data/fields";
|
import { TextField } from "data/fields";
|
||||||
import { em as $em, entity as $entity, text as $text } from "data/prototype";
|
import { em as $em, entity as $entity, text as $text } from "data/prototype";
|
||||||
@@ -6,13 +6,11 @@ import { Entity, EntityManager } from "data/entities";
|
|||||||
import { ManyToOneRelation } from "data/relations";
|
import { ManyToOneRelation } from "data/relations";
|
||||||
import { RepositoryEvents } from "data/events";
|
import { RepositoryEvents } from "data/events";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
type E = Kysely<any> | Transaction<any>;
|
type E = Kysely<any> | Transaction<any>;
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
beforeAll(() => disableConsoleLog());
|
afterAll(afterAllCleanup);
|
||||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
|
||||||
|
|
||||||
async function sleep(ms: number) {
|
async function sleep(ms: number) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Entity, EntityManager } from "data/entities";
|
import { Entity, EntityManager } from "data/entities";
|
||||||
import { ManyToManyRelation, ManyToOneRelation, PolymorphicRelation } from "data/relations";
|
import { ManyToManyRelation, ManyToOneRelation, PolymorphicRelation } from "data/relations";
|
||||||
import { TextField } from "data/fields";
|
import { TextField } from "data/fields";
|
||||||
@@ -6,10 +6,6 @@ import * as proto from "data/prototype";
|
|||||||
import { WithBuilder } from "data/entities/query/WithBuilder";
|
import { WithBuilder } from "data/entities/query/WithBuilder";
|
||||||
import { schemaToEm } from "../../helper";
|
import { schemaToEm } from "../../helper";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
|
|
||||||
|
|||||||
@@ -23,4 +23,11 @@ describe("FieldIndex", async () => {
|
|||||||
expect(index.name).toEqual("idx_test_name");
|
expect(index.name).toEqual("idx_test_name");
|
||||||
expect(index.unique).toEqual(false);
|
expect(index.unique).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("it fails on non-unique", async () => {
|
||||||
|
const field = new TestField("name", { required: false });
|
||||||
|
|
||||||
|
expect(() => new EntityIndex(entity, [field], true)).toThrowError();
|
||||||
|
expect(() => new EntityIndex(entity, [field])).toBeDefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ import {
|
|||||||
type BaseRelationConfig,
|
type BaseRelationConfig,
|
||||||
EntityRelation,
|
EntityRelation,
|
||||||
EntityRelationAnchor,
|
EntityRelationAnchor,
|
||||||
ManyToManyRelation,
|
|
||||||
RelationTypes,
|
RelationTypes,
|
||||||
} from "data/relations";
|
} from "data/relations";
|
||||||
import * as proto from "data/prototype";
|
|
||||||
|
|
||||||
class TestEntityRelation extends EntityRelation {
|
class TestEntityRelation extends EntityRelation {
|
||||||
constructor(config?: BaseRelationConfig) {
|
constructor(config?: BaseRelationConfig) {
|
||||||
@@ -77,15 +75,4 @@ describe("[data] EntityRelation", async () => {
|
|||||||
const relation2 = new TestEntityRelation({ required: true });
|
const relation2 = new TestEntityRelation({ required: true });
|
||||||
expect(relation2.required).toBe(true);
|
expect(relation2.required).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("correctly produces the relation name", async () => {
|
|
||||||
const relation = new ManyToManyRelation(new Entity("apps"), new Entity("organizations"));
|
|
||||||
expect(relation.getName()).not.toContain(",");
|
|
||||||
expect(relation.getName()).toBe("mn_apps_organizations");
|
|
||||||
|
|
||||||
const relation2 = new ManyToManyRelation(new Entity("apps"), new Entity("organizations"), {
|
|
||||||
connectionTableMappedName: "appOrganizations",
|
|
||||||
});
|
|
||||||
expect(relation2.getName()).toBe("mn_apps_organizations_appOrganizations");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import { describe, it, expect } from "bun:test";
|
|
||||||
import * as sDirect from "jsonv-ts";
|
|
||||||
import { s as sFromBknd } from "bknd/utils";
|
|
||||||
|
|
||||||
describe("jsonv-ts resolution", () => {
|
|
||||||
it("should resolve to a single instance", () => {
|
|
||||||
const sameNamespace = sDirect === (sFromBknd as unknown as typeof sDirect);
|
|
||||||
// If this fails, two instances are being loaded via different specifiers/paths
|
|
||||||
expect(sameNamespace).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should resolve specifiers to a single package path", async () => {
|
|
||||||
const base = await import.meta.resolve("jsonv-ts");
|
|
||||||
const hono = await import.meta.resolve("jsonv-ts/hono");
|
|
||||||
const mcp = await import.meta.resolve("jsonv-ts/mcp");
|
|
||||||
expect(typeof base).toBe("string");
|
|
||||||
expect(typeof hono).toBe("string");
|
|
||||||
expect(typeof mcp).toBe("string");
|
|
||||||
// They can be different files (subpath exports), but they should share the same package root
|
|
||||||
const pkgRoot = (p: string) => p.slice(0, p.lastIndexOf("jsonv-ts") + "jsonv-ts".length);
|
|
||||||
expect(pkgRoot(base)).toBe(pkgRoot(hono));
|
|
||||||
expect(pkgRoot(base)).toBe(pkgRoot(mcp));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
+22
-2
@@ -5,7 +5,7 @@ import { format as sqlFormat } from "sql-formatter";
|
|||||||
import type { em as protoEm } from "../src/data/prototype";
|
import type { em as protoEm } from "../src/data/prototype";
|
||||||
import { writeFile } from "node:fs/promises";
|
import { writeFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { slugify } from "bknd/utils";
|
import { slugify } from "core/utils/strings";
|
||||||
import { type Connection, SqliteLocalConnection } from "data/connection";
|
import { type Connection, SqliteLocalConnection } from "data/connection";
|
||||||
import { EntityManager } from "data/entities/EntityManager";
|
import { EntityManager } from "data/entities/EntityManager";
|
||||||
|
|
||||||
@@ -39,6 +39,26 @@ export function getLocalLibsqlConnection() {
|
|||||||
return { url: "http://127.0.0.1:8080" };
|
return { url: "http://127.0.0.1:8080" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ConsoleSeverity = "debug" | "log" | "warn" | "error";
|
||||||
|
const _oldConsoles = {
|
||||||
|
debug: console.debug,
|
||||||
|
log: console.log,
|
||||||
|
warn: console.warn,
|
||||||
|
error: console.error,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function disableConsoleLog(severities: ConsoleSeverity[] = ["debug", "log", "warn"]) {
|
||||||
|
severities.forEach((severity) => {
|
||||||
|
console[severity] = () => null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function enableConsoleLog() {
|
||||||
|
Object.entries(_oldConsoles).forEach(([severity, fn]) => {
|
||||||
|
console[severity as ConsoleSeverity] = fn;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function compileQb(qb: SelectQueryBuilder<any, any, any>) {
|
export function compileQb(qb: SelectQueryBuilder<any, any, any>) {
|
||||||
const { sql, parameters } = qb.compile();
|
const { sql, parameters } = qb.compile();
|
||||||
return { sql, parameters };
|
return { sql, parameters };
|
||||||
@@ -46,7 +66,7 @@ export function compileQb(qb: SelectQueryBuilder<any, any, any>) {
|
|||||||
|
|
||||||
export function prettyPrintQb(qb: SelectQueryBuilder<any, any, any>) {
|
export function prettyPrintQb(qb: SelectQueryBuilder<any, any, any>) {
|
||||||
const { sql, parameters } = qb.compile();
|
const { sql, parameters } = qb.compile();
|
||||||
console.info("$", sqlFormat(sql), "\n[params]", parameters);
|
console.log("$", sqlFormat(sql), "\n[params]", parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function schemaToEm(s: ReturnType<typeof protoEm>, conn?: Connection): EntityManager<any> {
|
export function schemaToEm(s: ReturnType<typeof protoEm>, conn?: Connection): EntityManager<any> {
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
import { afterAll, afterEach, beforeAll, describe, expect, it } from "bun:test";
|
||||||
import { App, createApp, type AuthResponse } from "../../src";
|
import { App, createApp } from "../../src";
|
||||||
|
import type { AuthResponse } from "../../src/auth";
|
||||||
import { auth } from "../../src/auth/middlewares";
|
import { auth } from "../../src/auth/middlewares";
|
||||||
import { randomString, secureRandomString, withDisabledConsole } from "../../src/core/utils";
|
import { randomString, secureRandomString, withDisabledConsole } from "../../src/core/utils";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
import { disableConsoleLog, enableConsoleLog, getDummyConnection } from "../helper";
|
||||||
import { getDummyConnection } from "../helper";
|
|
||||||
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
|
afterEach(afterAllCleanup);
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
beforeAll(disableConsoleLog);
|
||||||
afterAll(enableConsoleLog);
|
afterAll(enableConsoleLog);
|
||||||
@@ -63,10 +66,9 @@ const configs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function createAuthApp() {
|
function createAuthApp() {
|
||||||
const { dummyConnection } = getDummyConnection();
|
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
connection: dummyConnection,
|
connection: dummyConnection,
|
||||||
config: {
|
initialConfig: {
|
||||||
auth: configs.auth,
|
auth: configs.auth,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -149,8 +151,8 @@ describe("integration auth", () => {
|
|||||||
|
|
||||||
const { data: users } = await app.em.repository("users").findMany();
|
const { data: users } = await app.em.repository("users").findMany();
|
||||||
expect(users.length).toBe(2);
|
expect(users.length).toBe(2);
|
||||||
expect(users[0]?.email).toBe(configs.users.normal.email);
|
expect(users[0].email).toBe(configs.users.normal.email);
|
||||||
expect(users[1]?.email).toBe(configs.users.admin.email);
|
expect(users[1].email).toBe(configs.users.admin.email);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should log you in with API", async () => {
|
it("should log you in with API", async () => {
|
||||||
@@ -221,7 +223,7 @@ describe("integration auth", () => {
|
|||||||
|
|
||||||
app.server.get("/get", auth(), async (c) => {
|
app.server.get("/get", auth(), async (c) => {
|
||||||
return c.json({
|
return c.json({
|
||||||
user: c.get("auth")?.user ?? null,
|
user: c.get("auth").user ?? null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
app.server.get("/wait", auth(), async (c) => {
|
app.server.get("/wait", auth(), async (c) => {
|
||||||
@@ -240,7 +242,7 @@ describe("integration auth", () => {
|
|||||||
{
|
{
|
||||||
await new Promise((r) => setTimeout(r, 10));
|
await new Promise((r) => setTimeout(r, 10));
|
||||||
const res = await app.server.request("/get");
|
const res = await app.server.request("/get");
|
||||||
const data = (await res.json()) as any;
|
const data = await res.json();
|
||||||
expect(data.user).toBe(null);
|
expect(data.user).toBe(null);
|
||||||
expect(await $fns.me()).toEqual({ user: null as any });
|
expect(await $fns.me()).toEqual({ user: null as any });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
import { describe, expect, it } from "bun:test";
|
||||||
import { createApp } from "core/test/utils";
|
import { createApp } from "core/test/utils";
|
||||||
import { Api } from "../../src/Api";
|
import { Api } from "../../src/Api";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
describe("integration config", () => {
|
describe("integration config", () => {
|
||||||
it("should create an entity", async () => {
|
it("should create an entity", async () => {
|
||||||
|
|||||||
@@ -6,20 +6,17 @@ import { createApp } from "core/test/utils";
|
|||||||
import { mergeObject, randomString } from "../../src/core/utils";
|
import { mergeObject, randomString } from "../../src/core/utils";
|
||||||
import type { TAppMediaConfig } from "../../src/media/media-schema";
|
import type { TAppMediaConfig } from "../../src/media/media-schema";
|
||||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||||
import { assetsPath, assetsTmpPath } from "../helper";
|
import { assetsPath, assetsTmpPath, disableConsoleLog, enableConsoleLog } from "../helper";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
//disableConsoleLog();
|
|
||||||
registries.media.register("local", StorageLocalAdapter);
|
registries.media.register("local", StorageLocalAdapter);
|
||||||
});
|
});
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
const path = `${assetsPath}/image.png`;
|
const path = `${assetsPath}/image.png`;
|
||||||
|
|
||||||
async function makeApp(mediaOverride: Partial<TAppMediaConfig> = {}) {
|
async function makeApp(mediaOverride: Partial<TAppMediaConfig> = {}) {
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
config: {
|
initialConfig: {
|
||||||
media: mergeObject(
|
media: mergeObject(
|
||||||
{
|
{
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -43,6 +40,9 @@ function makeName(ext: string) {
|
|||||||
return randomString(10) + "." + ext;
|
return randomString(10) + "." + ext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
beforeAll(disableConsoleLog);
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
describe("MediaController", () => {
|
describe("MediaController", () => {
|
||||||
test("accepts direct", async () => {
|
test("accepts direct", async () => {
|
||||||
const app = await makeApp();
|
const app = await makeApp();
|
||||||
@@ -94,38 +94,4 @@ describe("MediaController", () => {
|
|||||||
expect(res.status).toBe(413);
|
expect(res.status).toBe(413);
|
||||||
expect(await Bun.file(assetsTmpPath + "/" + name).exists()).toBe(false);
|
expect(await Bun.file(assetsTmpPath + "/" + name).exists()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("audio files", async () => {
|
|
||||||
const app = await makeApp();
|
|
||||||
const file = Bun.file(`${assetsPath}/test.mp3`);
|
|
||||||
const name = makeName("mp3");
|
|
||||||
const res = await app.server.request("/api/media/upload/" + name, {
|
|
||||||
method: "POST",
|
|
||||||
body: file,
|
|
||||||
});
|
|
||||||
const result = (await res.json()) as any;
|
|
||||||
expect(result.data.mime_type).toStartWith("audio/mpeg");
|
|
||||||
expect(result.name).toBe(name);
|
|
||||||
|
|
||||||
const destFile = Bun.file(assetsTmpPath + "/" + name);
|
|
||||||
expect(destFile.exists()).resolves.toBe(true);
|
|
||||||
await destFile.delete();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("text files", async () => {
|
|
||||||
const app = await makeApp();
|
|
||||||
const file = Bun.file(`${assetsPath}/test.txt`);
|
|
||||||
const name = makeName("txt");
|
|
||||||
const res = await app.server.request("/api/media/upload/" + name, {
|
|
||||||
method: "POST",
|
|
||||||
body: file,
|
|
||||||
});
|
|
||||||
const result = (await res.json()) as any;
|
|
||||||
expect(result.data.mime_type).toStartWith("text/plain");
|
|
||||||
expect(result.name).toBe(name);
|
|
||||||
|
|
||||||
const destFile = Bun.file(assetsTmpPath + "/" + name);
|
|
||||||
expect(destFile.exists()).resolves.toBe(true);
|
|
||||||
await destFile.delete();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -71,8 +71,6 @@ describe("media/mime-types", () => {
|
|||||||
["application/zip", "zip"],
|
["application/zip", "zip"],
|
||||||
["text/tab-separated-values", "tsv"],
|
["text/tab-separated-values", "tsv"],
|
||||||
["application/zip", "zip"],
|
["application/zip", "zip"],
|
||||||
["application/pdf", "pdf"],
|
|
||||||
["audio/mpeg", "mp3"],
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
for (const [mime, ext] of tests) {
|
for (const [mime, ext] of tests) {
|
||||||
@@ -90,9 +88,6 @@ describe("media/mime-types", () => {
|
|||||||
["image.jpeg", "jpeg"],
|
["image.jpeg", "jpeg"],
|
||||||
["-473Wx593H-466453554-black-MODEL.jpg", "jpg"],
|
["-473Wx593H-466453554-black-MODEL.jpg", "jpg"],
|
||||||
["-473Wx593H-466453554-black-MODEL.avif", "avif"],
|
["-473Wx593H-466453554-black-MODEL.avif", "avif"],
|
||||||
["file.pdf", "pdf"],
|
|
||||||
["file.mp3", "mp3"],
|
|
||||||
["robots.txt", "txt"],
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
for (const [filename, ext] of tests) {
|
for (const [filename, ext] of tests) {
|
||||||
@@ -107,36 +102,4 @@ describe("media/mime-types", () => {
|
|||||||
const [, ext] = getRandomizedFilename(file).split(".");
|
const [, ext] = getRandomizedFilename(file).split(".");
|
||||||
expect(ext).toBe("jpg");
|
expect(ext).toBe("jpg");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getRandomizedFilename with body", async () => {
|
|
||||||
// should keep "pdf"
|
|
||||||
const [, ext] = getRandomizedFilename(
|
|
||||||
new File([""], "file.pdf", { type: "application/pdf" }),
|
|
||||||
).split(".");
|
|
||||||
expect(ext).toBe("pdf");
|
|
||||||
|
|
||||||
{
|
|
||||||
// no ext, should use "pdf" only for known formats
|
|
||||||
const [, ext] = getRandomizedFilename(
|
|
||||||
new File([""], "file", { type: "application/pdf" }),
|
|
||||||
).split(".");
|
|
||||||
expect(ext).toBe("pdf");
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// wrong ext, should keep the wrong one
|
|
||||||
const [, ext] = getRandomizedFilename(
|
|
||||||
new File([""], "file.what", { type: "application/pdf" }),
|
|
||||||
).split(".");
|
|
||||||
expect(ext).toBe("what");
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// txt
|
|
||||||
const [, ext] = getRandomizedFilename(
|
|
||||||
new File([""], "file.txt", { type: "text/plain" }),
|
|
||||||
).split(".");
|
|
||||||
expect(ext).toBe("txt");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,14 +3,11 @@ import { createApp } from "core/test/utils";
|
|||||||
import { AuthController } from "../../src/auth/api/AuthController";
|
import { AuthController } from "../../src/auth/api/AuthController";
|
||||||
import { em, entity, make, text } from "data/prototype";
|
import { em, entity, make, text } from "data/prototype";
|
||||||
import { AppAuth, type ModuleBuildContext } from "modules";
|
import { AppAuth, type ModuleBuildContext } from "modules";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "../helper";
|
||||||
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
describe("AppAuth", () => {
|
describe("AppAuth", () => {
|
||||||
test.skip("...", () => {
|
test.only("...", () => {
|
||||||
const auth = new AppAuth({});
|
const auth = new AppAuth({});
|
||||||
console.log(auth.toJSON());
|
console.log(auth.toJSON());
|
||||||
console.log(auth.config);
|
console.log(auth.config);
|
||||||
@@ -150,7 +147,7 @@ describe("AppAuth", () => {
|
|||||||
|
|
||||||
test("registers auth middleware for bknd routes only", async () => {
|
test("registers auth middleware for bknd routes only", async () => {
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
config: {
|
initialConfig: {
|
||||||
auth: {
|
auth: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
jwt: {
|
jwt: {
|
||||||
@@ -180,7 +177,7 @@ describe("AppAuth", () => {
|
|||||||
|
|
||||||
test("should allow additional user fields", async () => {
|
test("should allow additional user fields", async () => {
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
config: {
|
initialConfig: {
|
||||||
auth: {
|
auth: {
|
||||||
entity_name: "users",
|
entity_name: "users",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -204,7 +201,7 @@ describe("AppAuth", () => {
|
|||||||
|
|
||||||
test("ensure user field configs is always correct", async () => {
|
test("ensure user field configs is always correct", async () => {
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
config: {
|
initialConfig: {
|
||||||
auth: {
|
auth: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { AppMedia } from "../../src/media/AppMedia";
|
|||||||
import { moduleTestSuite } from "./module-test-suite";
|
import { moduleTestSuite } from "./module-test-suite";
|
||||||
|
|
||||||
describe("AppMedia", () => {
|
describe("AppMedia", () => {
|
||||||
test.skip("...", () => {
|
test.only("...", () => {
|
||||||
const media = new AppMedia();
|
const media = new AppMedia();
|
||||||
console.log(media.toJSON());
|
console.log(media.toJSON());
|
||||||
});
|
});
|
||||||
@@ -18,7 +18,7 @@ describe("AppMedia", () => {
|
|||||||
registries.media.register("local", StorageLocalAdapter);
|
registries.media.register("local", StorageLocalAdapter);
|
||||||
|
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
config: {
|
initialConfig: {
|
||||||
media: {
|
media: {
|
||||||
entity_name: "media",
|
entity_name: "media",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
import { it, expect, describe } from "bun:test";
|
|
||||||
import { DbModuleManager } from "modules/db/DbModuleManager";
|
|
||||||
import { getDummyConnection } from "../helper";
|
|
||||||
import { TABLE_NAME } from "modules/db/migrations";
|
|
||||||
|
|
||||||
describe("DbModuleManager", () => {
|
|
||||||
it("should extract secrets", async () => {
|
|
||||||
const { dummyConnection } = getDummyConnection();
|
|
||||||
const m = new DbModuleManager(dummyConnection, {
|
|
||||||
initial: {
|
|
||||||
auth: {
|
|
||||||
enabled: true,
|
|
||||||
jwt: {
|
|
||||||
secret: "test",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await m.build();
|
|
||||||
expect(m.toJSON(true).auth.jwt.secret).toBe("test");
|
|
||||||
await m.save();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should work with initial secrets", async () => {
|
|
||||||
const { dummyConnection } = getDummyConnection();
|
|
||||||
const db = dummyConnection.kysely;
|
|
||||||
const m = new DbModuleManager(dummyConnection, {
|
|
||||||
initial: {
|
|
||||||
auth: {
|
|
||||||
enabled: true,
|
|
||||||
jwt: {
|
|
||||||
secret: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
secrets: {
|
|
||||||
"auth.jwt.secret": "test",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await m.build();
|
|
||||||
expect(m.toJSON(true).auth.jwt.secret).toBe("test");
|
|
||||||
|
|
||||||
const getSecrets = () =>
|
|
||||||
db
|
|
||||||
.selectFrom(TABLE_NAME)
|
|
||||||
.selectAll()
|
|
||||||
.where("type", "=", "secrets")
|
|
||||||
.executeTakeFirst()
|
|
||||||
.then((r) => r?.json);
|
|
||||||
|
|
||||||
expect(await getSecrets()).toEqual({ "auth.jwt.secret": "test" });
|
|
||||||
|
|
||||||
// also after rebuild
|
|
||||||
await m.build();
|
|
||||||
await m.save();
|
|
||||||
expect(await getSecrets()).toEqual({ "auth.jwt.secret": "test" });
|
|
||||||
|
|
||||||
// and ignore if already present
|
|
||||||
const m2 = new DbModuleManager(dummyConnection, {
|
|
||||||
initial: {
|
|
||||||
auth: {
|
|
||||||
enabled: true,
|
|
||||||
jwt: {
|
|
||||||
secret: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
secrets: {
|
|
||||||
"auth.jwt.secret": "something completely different",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await m2.build();
|
|
||||||
await m2.save();
|
|
||||||
expect(await getSecrets()).toEqual({ "auth.jwt.secret": "test" });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,19 +1,14 @@
|
|||||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||||
|
|
||||||
import { Module } from "modules/Module";
|
import { Module } from "modules/Module";
|
||||||
import { getDefaultConfig } from "modules/ModuleManager";
|
import { type ConfigTable, getDefaultConfig, ModuleManager } from "modules/ModuleManager";
|
||||||
import { type ConfigTable, DbModuleManager as ModuleManager } from "modules/db/DbModuleManager";
|
import { CURRENT_VERSION, TABLE_NAME } from "modules/migrations";
|
||||||
|
|
||||||
import { CURRENT_VERSION, TABLE_NAME } from "modules/db/migrations";
|
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
import { s, stripMark } from "core/utils/schema";
|
import { s, stripMark } from "core/utils/schema";
|
||||||
import { Connection } from "data/connection/Connection";
|
import { Connection } from "data/connection/Connection";
|
||||||
import { entity, text } from "data/prototype";
|
import { entity, text } from "data/prototype";
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
describe("ModuleManager", async () => {
|
describe("ModuleManager", async () => {
|
||||||
test("s1: no config, no build", async () => {
|
test("s1: no config, no build", async () => {
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
@@ -138,7 +133,7 @@ describe("ModuleManager", async () => {
|
|||||||
const db = c2.dummyConnection.kysely;
|
const db = c2.dummyConnection.kysely;
|
||||||
|
|
||||||
const mm2 = new ModuleManager(c2.dummyConnection, {
|
const mm2 = new ModuleManager(c2.dummyConnection, {
|
||||||
initial: { version: version - 1, ...json } as any,
|
initial: { version: version - 1, ...json },
|
||||||
});
|
});
|
||||||
await mm2.syncConfigTable();
|
await mm2.syncConfigTable();
|
||||||
await db
|
await db
|
||||||
|
|||||||
@@ -1,22 +1,14 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { App, type InitialModuleConfigs, createApp } from "/";
|
import { type InitialModuleConfigs, createApp } from "../../../src";
|
||||||
|
|
||||||
import { type Kysely, sql } from "kysely";
|
import { type Kysely, sql } from "kysely";
|
||||||
import { getDummyConnection } from "../../helper";
|
import { getDummyConnection } from "../../helper";
|
||||||
import v7 from "./samples/v7.json";
|
import v7 from "./samples/v7.json";
|
||||||
import v8 from "./samples/v8.json";
|
import v8 from "./samples/v8.json";
|
||||||
import v8_2 from "./samples/v8-2.json";
|
import v8_2 from "./samples/v8-2.json";
|
||||||
import v9 from "./samples/v9.json";
|
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
// app expects migratable config to be present in database
|
// app expects migratable config to be present in database
|
||||||
async function createVersionedApp(
|
async function createVersionedApp(config: InitialModuleConfigs | any) {
|
||||||
config: InitialModuleConfigs | any,
|
|
||||||
opts?: { beforeCreateApp?: (db: Kysely<any>) => Promise<void> },
|
|
||||||
) {
|
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
|
|
||||||
if (!("version" in config)) throw new Error("config must have a version");
|
if (!("version" in config)) throw new Error("config must have a version");
|
||||||
@@ -42,10 +34,6 @@ async function createVersionedApp(
|
|||||||
})
|
})
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
if (opts?.beforeCreateApp) {
|
|
||||||
await opts.beforeCreateApp(db);
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
connection: dummyConnection,
|
connection: dummyConnection,
|
||||||
});
|
});
|
||||||
@@ -53,19 +41,6 @@ async function createVersionedApp(
|
|||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getRawConfig(
|
|
||||||
app: App,
|
|
||||||
opts?: { version?: number; types?: ("config" | "diff" | "backup" | "secrets")[] },
|
|
||||||
) {
|
|
||||||
const db = app.em.connection.kysely;
|
|
||||||
return await db
|
|
||||||
.selectFrom("__bknd")
|
|
||||||
.selectAll()
|
|
||||||
.$if(!!opts?.version, (qb) => qb.where("version", "=", opts?.version))
|
|
||||||
.$if((opts?.types?.length ?? 0) > 0, (qb) => qb.where("type", "in", opts?.types))
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("Migrations", () => {
|
describe("Migrations", () => {
|
||||||
/**
|
/**
|
||||||
* updated auth strategies to have "enabled" prop
|
* updated auth strategies to have "enabled" prop
|
||||||
@@ -103,30 +78,4 @@ describe("Migrations", () => {
|
|||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
expect(app.toJSON(true).server.admin).toBeUndefined();
|
expect(app.toJSON(true).server.admin).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("migration from 9 to 10", async () => {
|
|
||||||
expect(v9.version).toBe(9);
|
|
||||||
|
|
||||||
const app = await createVersionedApp(v9);
|
|
||||||
|
|
||||||
expect(app.version()).toBeGreaterThan(9);
|
|
||||||
// @ts-expect-error
|
|
||||||
expect(app.toJSON(true).media.adapter.config.secret_access_key).toBe(
|
|
||||||
"^^s3.secret_access_key^^",
|
|
||||||
);
|
|
||||||
const [config, secrets] = (await getRawConfig(app, {
|
|
||||||
version: 10,
|
|
||||||
types: ["config", "secrets"],
|
|
||||||
})) as any;
|
|
||||||
|
|
||||||
expect(config.json.auth.jwt.secret).toBe("");
|
|
||||||
expect(config.json.media.adapter.config.access_key).toBe("");
|
|
||||||
expect(config.json.media.adapter.config.secret_access_key).toBe("");
|
|
||||||
|
|
||||||
expect(secrets.json["auth.jwt.secret"]).toBe("^^jwt.secret^^");
|
|
||||||
expect(secrets.json["media.adapter.config.access_key"]).toBe("^^s3.access_key^^");
|
|
||||||
expect(secrets.json["media.adapter.config.secret_access_key"]).toBe(
|
|
||||||
"^^s3.secret_access_key^^",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,612 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 9,
|
|
||||||
"server": {
|
|
||||||
"cors": {
|
|
||||||
"origin": "*",
|
|
||||||
"allow_methods": ["GET", "POST", "PATCH", "PUT", "DELETE"],
|
|
||||||
"allow_headers": [
|
|
||||||
"Content-Type",
|
|
||||||
"Content-Length",
|
|
||||||
"Authorization",
|
|
||||||
"Accept"
|
|
||||||
],
|
|
||||||
"allow_credentials": true
|
|
||||||
},
|
|
||||||
"mcp": { "enabled": false, "path": "/api/system/mcp" }
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"basepath": "/api/data",
|
|
||||||
"default_primary_format": "integer",
|
|
||||||
"entities": {
|
|
||||||
"media": {
|
|
||||||
"type": "system",
|
|
||||||
"fields": {
|
|
||||||
"id": {
|
|
||||||
"type": "primary",
|
|
||||||
"config": {
|
|
||||||
"format": "integer",
|
|
||||||
"fillable": false,
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"path": { "type": "text", "config": { "required": true } },
|
|
||||||
"folder": {
|
|
||||||
"type": "boolean",
|
|
||||||
"config": {
|
|
||||||
"default_value": false,
|
|
||||||
"hidden": true,
|
|
||||||
"fillable": ["create"],
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mime_type": { "type": "text", "config": { "required": false } },
|
|
||||||
"size": { "type": "number", "config": { "required": false } },
|
|
||||||
"scope": {
|
|
||||||
"type": "text",
|
|
||||||
"config": {
|
|
||||||
"hidden": true,
|
|
||||||
"fillable": ["create"],
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"etag": { "type": "text", "config": { "required": false } },
|
|
||||||
"modified_at": {
|
|
||||||
"type": "date",
|
|
||||||
"config": { "type": "datetime", "required": false }
|
|
||||||
},
|
|
||||||
"reference": { "type": "text", "config": { "required": false } },
|
|
||||||
"entity_id": { "type": "number", "config": { "required": false } },
|
|
||||||
"metadata": { "type": "json", "config": { "required": false } }
|
|
||||||
},
|
|
||||||
"config": { "sort_field": "id", "sort_dir": "asc" }
|
|
||||||
},
|
|
||||||
"users": {
|
|
||||||
"type": "system",
|
|
||||||
"fields": {
|
|
||||||
"id": {
|
|
||||||
"type": "primary",
|
|
||||||
"config": {
|
|
||||||
"format": "integer",
|
|
||||||
"fillable": false,
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"email": { "type": "text", "config": { "required": true } },
|
|
||||||
"strategy": {
|
|
||||||
"type": "enum",
|
|
||||||
"config": {
|
|
||||||
"options": { "type": "strings", "values": ["password"] },
|
|
||||||
"required": true,
|
|
||||||
"hidden": ["update", "form"],
|
|
||||||
"fillable": ["create"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"strategy_value": {
|
|
||||||
"type": "text",
|
|
||||||
"config": {
|
|
||||||
"fillable": ["create"],
|
|
||||||
"hidden": ["read", "table", "update", "form"],
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"role": {
|
|
||||||
"type": "enum",
|
|
||||||
"config": {
|
|
||||||
"options": { "type": "strings", "values": ["admin", "guest"] },
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"age": {
|
|
||||||
"type": "enum",
|
|
||||||
"config": {
|
|
||||||
"options": {
|
|
||||||
"type": "strings",
|
|
||||||
"values": ["18-24", "25-34", "35-44", "45-64", "65+"]
|
|
||||||
},
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"height": { "type": "number", "config": { "required": false } },
|
|
||||||
"gender": {
|
|
||||||
"type": "enum",
|
|
||||||
"config": {
|
|
||||||
"options": { "type": "strings", "values": ["male", "female"] },
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"config": { "sort_field": "id", "sort_dir": "asc" }
|
|
||||||
},
|
|
||||||
"avatars": {
|
|
||||||
"type": "regular",
|
|
||||||
"fields": {
|
|
||||||
"id": {
|
|
||||||
"type": "primary",
|
|
||||||
"config": {
|
|
||||||
"format": "integer",
|
|
||||||
"fillable": false,
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"identifier": { "type": "text", "config": { "required": false } },
|
|
||||||
"payload": {
|
|
||||||
"type": "json",
|
|
||||||
"config": { "required": false, "hidden": ["table"] }
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"type": "date",
|
|
||||||
"config": { "type": "datetime", "required": false }
|
|
||||||
},
|
|
||||||
"started_at": {
|
|
||||||
"type": "date",
|
|
||||||
"config": { "type": "datetime", "required": false }
|
|
||||||
},
|
|
||||||
"completed_at": {
|
|
||||||
"type": "date",
|
|
||||||
"config": { "type": "datetime", "required": false }
|
|
||||||
},
|
|
||||||
"input": {
|
|
||||||
"type": "media",
|
|
||||||
"config": {
|
|
||||||
"required": false,
|
|
||||||
"fillable": ["update"],
|
|
||||||
"hidden": false,
|
|
||||||
"mime_types": [],
|
|
||||||
"virtual": true,
|
|
||||||
"entity": "avatars"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"output": {
|
|
||||||
"type": "media",
|
|
||||||
"config": {
|
|
||||||
"required": false,
|
|
||||||
"fillable": ["update"],
|
|
||||||
"hidden": false,
|
|
||||||
"mime_types": [],
|
|
||||||
"virtual": true,
|
|
||||||
"entity": "avatars"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"users_id": {
|
|
||||||
"type": "relation",
|
|
||||||
"config": {
|
|
||||||
"label": "Users",
|
|
||||||
"required": false,
|
|
||||||
"reference": "users",
|
|
||||||
"target": "users",
|
|
||||||
"target_field": "id",
|
|
||||||
"target_field_type": "integer",
|
|
||||||
"on_delete": "set null"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"config": { "sort_field": "id", "sort_dir": "desc" }
|
|
||||||
},
|
|
||||||
"tryons": {
|
|
||||||
"type": "regular",
|
|
||||||
"fields": {
|
|
||||||
"id": {
|
|
||||||
"type": "primary",
|
|
||||||
"config": {
|
|
||||||
"format": "integer",
|
|
||||||
"fillable": false,
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"type": "date",
|
|
||||||
"config": { "type": "datetime", "required": false }
|
|
||||||
},
|
|
||||||
"completed_at": {
|
|
||||||
"type": "date",
|
|
||||||
"config": { "type": "datetime", "required": false }
|
|
||||||
},
|
|
||||||
"avatars_id": {
|
|
||||||
"type": "relation",
|
|
||||||
"config": {
|
|
||||||
"label": "Avatars",
|
|
||||||
"required": false,
|
|
||||||
"reference": "avatars",
|
|
||||||
"target": "avatars",
|
|
||||||
"target_field": "id",
|
|
||||||
"target_field_type": "integer",
|
|
||||||
"on_delete": "set null"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"users_id": {
|
|
||||||
"type": "relation",
|
|
||||||
"config": {
|
|
||||||
"label": "Users",
|
|
||||||
"required": false,
|
|
||||||
"reference": "users",
|
|
||||||
"target": "users",
|
|
||||||
"target_field": "id",
|
|
||||||
"target_field_type": "integer",
|
|
||||||
"on_delete": "set null"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"output": {
|
|
||||||
"type": "media",
|
|
||||||
"config": {
|
|
||||||
"required": false,
|
|
||||||
"fillable": ["update"],
|
|
||||||
"hidden": false,
|
|
||||||
"mime_types": [],
|
|
||||||
"virtual": true,
|
|
||||||
"entity": "tryons",
|
|
||||||
"max_items": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"products_id": {
|
|
||||||
"type": "relation",
|
|
||||||
"config": {
|
|
||||||
"label": "Products",
|
|
||||||
"required": false,
|
|
||||||
"reference": "products",
|
|
||||||
"target": "products",
|
|
||||||
"target_field": "id",
|
|
||||||
"target_field_type": "integer",
|
|
||||||
"on_delete": "set null"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"payload": {
|
|
||||||
"type": "json",
|
|
||||||
"config": { "required": false, "hidden": ["table"] }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"config": { "sort_field": "id", "sort_dir": "desc" }
|
|
||||||
},
|
|
||||||
"products": {
|
|
||||||
"type": "regular",
|
|
||||||
"fields": {
|
|
||||||
"id": {
|
|
||||||
"type": "primary",
|
|
||||||
"config": {
|
|
||||||
"format": "integer",
|
|
||||||
"fillable": false,
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"enabled": { "type": "boolean", "config": { "required": false } },
|
|
||||||
"title": { "type": "text", "config": { "required": false } },
|
|
||||||
"url": { "type": "text", "config": { "required": false } },
|
|
||||||
"image": {
|
|
||||||
"type": "media",
|
|
||||||
"config": {
|
|
||||||
"required": false,
|
|
||||||
"fillable": ["update"],
|
|
||||||
"hidden": false,
|
|
||||||
"mime_types": [],
|
|
||||||
"virtual": true,
|
|
||||||
"entity": "products",
|
|
||||||
"max_items": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"type": "date",
|
|
||||||
"config": { "type": "datetime", "required": false }
|
|
||||||
},
|
|
||||||
"sites_id": {
|
|
||||||
"type": "relation",
|
|
||||||
"config": {
|
|
||||||
"label": "Sites",
|
|
||||||
"required": false,
|
|
||||||
"reference": "sites",
|
|
||||||
"target": "sites",
|
|
||||||
"target_field": "id",
|
|
||||||
"target_field_type": "integer",
|
|
||||||
"on_delete": "set null"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"garment_type": {
|
|
||||||
"type": "enum",
|
|
||||||
"config": {
|
|
||||||
"options": {
|
|
||||||
"type": "strings",
|
|
||||||
"values": ["auto", "tops", "bottoms", "one-pieces"]
|
|
||||||
},
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"config": { "sort_field": "id", "sort_dir": "desc" }
|
|
||||||
},
|
|
||||||
"sites": {
|
|
||||||
"type": "regular",
|
|
||||||
"fields": {
|
|
||||||
"id": {
|
|
||||||
"type": "primary",
|
|
||||||
"config": {
|
|
||||||
"format": "integer",
|
|
||||||
"fillable": false,
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"origin": {
|
|
||||||
"type": "text",
|
|
||||||
"config": {
|
|
||||||
"pattern": "^(https?):\\/\\/([a-zA-Z0-9.-]+)(:\\d+)?$",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": { "type": "text", "config": { "required": false } },
|
|
||||||
"active": { "type": "boolean", "config": { "required": false } },
|
|
||||||
"logo": {
|
|
||||||
"type": "media",
|
|
||||||
"config": {
|
|
||||||
"required": false,
|
|
||||||
"fillable": ["update"],
|
|
||||||
"hidden": false,
|
|
||||||
"mime_types": [],
|
|
||||||
"virtual": true,
|
|
||||||
"entity": "sites",
|
|
||||||
"max_items": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"instructions": {
|
|
||||||
"type": "text",
|
|
||||||
"config": {
|
|
||||||
"html_config": {
|
|
||||||
"element": "textarea",
|
|
||||||
"props": { "rows": "2" }
|
|
||||||
},
|
|
||||||
"required": false,
|
|
||||||
"hidden": ["table"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"config": { "sort_field": "id", "sort_dir": "desc" }
|
|
||||||
},
|
|
||||||
"sessions": {
|
|
||||||
"type": "regular",
|
|
||||||
"fields": {
|
|
||||||
"id": {
|
|
||||||
"type": "primary",
|
|
||||||
"config": { "format": "uuid", "fillable": false, "required": false }
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"type": "date",
|
|
||||||
"config": { "type": "datetime", "required": true }
|
|
||||||
},
|
|
||||||
"claimed_at": {
|
|
||||||
"type": "date",
|
|
||||||
"config": { "type": "datetime", "required": false }
|
|
||||||
},
|
|
||||||
"url": { "type": "text", "config": { "required": false } },
|
|
||||||
"sites_id": {
|
|
||||||
"type": "relation",
|
|
||||||
"config": {
|
|
||||||
"label": "Sites",
|
|
||||||
"required": false,
|
|
||||||
"reference": "sites",
|
|
||||||
"target": "sites",
|
|
||||||
"target_field": "id",
|
|
||||||
"target_field_type": "integer",
|
|
||||||
"on_delete": "set null"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"users_id": {
|
|
||||||
"type": "relation",
|
|
||||||
"config": {
|
|
||||||
"label": "Users",
|
|
||||||
"required": false,
|
|
||||||
"reference": "users",
|
|
||||||
"target": "users",
|
|
||||||
"target_field": "id",
|
|
||||||
"target_field_type": "integer",
|
|
||||||
"on_delete": "set null"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"config": { "sort_field": "id", "sort_dir": "desc" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"relations": {
|
|
||||||
"poly_avatars_media_input": {
|
|
||||||
"type": "poly",
|
|
||||||
"source": "avatars",
|
|
||||||
"target": "media",
|
|
||||||
"config": { "mappedBy": "input" }
|
|
||||||
},
|
|
||||||
"poly_avatars_media_output": {
|
|
||||||
"type": "poly",
|
|
||||||
"source": "avatars",
|
|
||||||
"target": "media",
|
|
||||||
"config": { "mappedBy": "output" }
|
|
||||||
},
|
|
||||||
"n1_avatars_users": {
|
|
||||||
"type": "n:1",
|
|
||||||
"source": "avatars",
|
|
||||||
"target": "users",
|
|
||||||
"config": {
|
|
||||||
"mappedBy": "",
|
|
||||||
"inversedBy": "",
|
|
||||||
"required": false,
|
|
||||||
"with_limit": 5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"n1_tryons_avatars": {
|
|
||||||
"type": "n:1",
|
|
||||||
"source": "tryons",
|
|
||||||
"target": "avatars",
|
|
||||||
"config": {
|
|
||||||
"mappedBy": "",
|
|
||||||
"inversedBy": "",
|
|
||||||
"required": false,
|
|
||||||
"with_limit": 5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"n1_tryons_users": {
|
|
||||||
"type": "n:1",
|
|
||||||
"source": "tryons",
|
|
||||||
"target": "users",
|
|
||||||
"config": {
|
|
||||||
"mappedBy": "",
|
|
||||||
"inversedBy": "",
|
|
||||||
"required": false,
|
|
||||||
"with_limit": 5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"poly_tryons_media_output": {
|
|
||||||
"type": "poly",
|
|
||||||
"source": "tryons",
|
|
||||||
"target": "media",
|
|
||||||
"config": { "mappedBy": "output", "targetCardinality": 1 }
|
|
||||||
},
|
|
||||||
"poly_products_media_image": {
|
|
||||||
"type": "poly",
|
|
||||||
"source": "products",
|
|
||||||
"target": "media",
|
|
||||||
"config": { "mappedBy": "image", "targetCardinality": 1 }
|
|
||||||
},
|
|
||||||
"n1_tryons_products": {
|
|
||||||
"type": "n:1",
|
|
||||||
"source": "tryons",
|
|
||||||
"target": "products",
|
|
||||||
"config": {
|
|
||||||
"mappedBy": "",
|
|
||||||
"inversedBy": "",
|
|
||||||
"required": false,
|
|
||||||
"with_limit": 5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"poly_sites_media_logo": {
|
|
||||||
"type": "poly",
|
|
||||||
"source": "sites",
|
|
||||||
"target": "media",
|
|
||||||
"config": { "mappedBy": "logo", "targetCardinality": 1 }
|
|
||||||
},
|
|
||||||
"n1_sessions_sites": {
|
|
||||||
"type": "n:1",
|
|
||||||
"source": "sessions",
|
|
||||||
"target": "sites",
|
|
||||||
"config": {
|
|
||||||
"mappedBy": "",
|
|
||||||
"inversedBy": "",
|
|
||||||
"required": false,
|
|
||||||
"with_limit": 5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"n1_sessions_users": {
|
|
||||||
"type": "n:1",
|
|
||||||
"source": "sessions",
|
|
||||||
"target": "users",
|
|
||||||
"config": {
|
|
||||||
"mappedBy": "",
|
|
||||||
"inversedBy": "",
|
|
||||||
"required": false,
|
|
||||||
"with_limit": 5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"n1_products_sites": {
|
|
||||||
"type": "n:1",
|
|
||||||
"source": "products",
|
|
||||||
"target": "sites",
|
|
||||||
"config": {
|
|
||||||
"mappedBy": "",
|
|
||||||
"inversedBy": "",
|
|
||||||
"required": false,
|
|
||||||
"with_limit": 5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indices": {
|
|
||||||
"idx_unique_media_path": {
|
|
||||||
"entity": "media",
|
|
||||||
"fields": ["path"],
|
|
||||||
"unique": true
|
|
||||||
},
|
|
||||||
"idx_media_reference": {
|
|
||||||
"entity": "media",
|
|
||||||
"fields": ["reference"],
|
|
||||||
"unique": false
|
|
||||||
},
|
|
||||||
"idx_media_entity_id": {
|
|
||||||
"entity": "media",
|
|
||||||
"fields": ["entity_id"],
|
|
||||||
"unique": false
|
|
||||||
},
|
|
||||||
"idx_unique_users_email": {
|
|
||||||
"entity": "users",
|
|
||||||
"fields": ["email"],
|
|
||||||
"unique": true
|
|
||||||
},
|
|
||||||
"idx_users_strategy": {
|
|
||||||
"entity": "users",
|
|
||||||
"fields": ["strategy"],
|
|
||||||
"unique": false
|
|
||||||
},
|
|
||||||
"idx_users_strategy_value": {
|
|
||||||
"entity": "users",
|
|
||||||
"fields": ["strategy_value"],
|
|
||||||
"unique": false
|
|
||||||
},
|
|
||||||
"idx_sites_origin_active": {
|
|
||||||
"entity": "sites",
|
|
||||||
"fields": ["origin", "active"],
|
|
||||||
"unique": false
|
|
||||||
},
|
|
||||||
"idx_sites_active": {
|
|
||||||
"entity": "sites",
|
|
||||||
"fields": ["active"],
|
|
||||||
"unique": false
|
|
||||||
},
|
|
||||||
"idx_products_url": {
|
|
||||||
"entity": "products",
|
|
||||||
"fields": ["url"],
|
|
||||||
"unique": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"auth": {
|
|
||||||
"enabled": true,
|
|
||||||
"basepath": "/api/auth",
|
|
||||||
"entity_name": "users",
|
|
||||||
"allow_register": true,
|
|
||||||
"jwt": {
|
|
||||||
"secret": "^^jwt.secret^^",
|
|
||||||
"alg": "HS256",
|
|
||||||
"expires": 999999999,
|
|
||||||
"issuer": "issuer",
|
|
||||||
"fields": ["id", "email", "role"]
|
|
||||||
},
|
|
||||||
"cookie": {
|
|
||||||
"path": "/",
|
|
||||||
"sameSite": "none",
|
|
||||||
"secure": true,
|
|
||||||
"httpOnly": true,
|
|
||||||
"expires": 604800,
|
|
||||||
"partitioned": false,
|
|
||||||
"renew": true,
|
|
||||||
"pathSuccess": "/admin",
|
|
||||||
"pathLoggedOut": "/"
|
|
||||||
},
|
|
||||||
"strategies": {
|
|
||||||
"password": {
|
|
||||||
"enabled": true,
|
|
||||||
"type": "password",
|
|
||||||
"config": { "hashing": "sha256" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"guard": { "enabled": false },
|
|
||||||
"roles": {
|
|
||||||
"admin": { "implicit_allow": true },
|
|
||||||
"guest": { "is_default": true }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"media": {
|
|
||||||
"enabled": true,
|
|
||||||
"basepath": "/api/media",
|
|
||||||
"entity_name": "media",
|
|
||||||
"storage": { "body_max_size": 0 },
|
|
||||||
"adapter": {
|
|
||||||
"type": "s3",
|
|
||||||
"config": {
|
|
||||||
"access_key": "^^s3.access_key^^",
|
|
||||||
"secret_access_key": "^^s3.secret_access_key^^",
|
|
||||||
"url": "https://1234.r2.cloudflarestorage.com/bucket-name"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"flows": { "basepath": "/api/flows", "flows": {} }
|
|
||||||
}
|
|
||||||
@@ -2,12 +2,12 @@ import { beforeEach, describe, expect, it } from "bun:test";
|
|||||||
|
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { Guard } from "auth/authorize/Guard";
|
import { Guard } from "auth/authorize/Guard";
|
||||||
|
import { DebugLogger } from "core/utils/DebugLogger";
|
||||||
import { EventManager } from "core/events";
|
import { EventManager } from "core/events";
|
||||||
import { EntityManager } from "data/entities/EntityManager";
|
import { EntityManager } from "data/entities/EntityManager";
|
||||||
import { Module, type ModuleBuildContext } from "modules/Module";
|
import { Module, type ModuleBuildContext } from "modules/Module";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
import { ModuleHelper } from "modules/ModuleHelper";
|
import { ModuleHelper } from "modules/ModuleHelper";
|
||||||
import { DebugLogger, McpServer } from "bknd/utils";
|
|
||||||
|
|
||||||
export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildContext {
|
export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildContext {
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
@@ -19,7 +19,6 @@ export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildCon
|
|||||||
guard: new Guard(),
|
guard: new Guard(),
|
||||||
flags: Module.ctx_flags,
|
flags: Module.ctx_flags,
|
||||||
logger: new DebugLogger(false),
|
logger: new DebugLogger(false),
|
||||||
mcp: new McpServer(),
|
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -102,9 +102,7 @@ describe("json form", () => {
|
|||||||
] satisfies [string, Exclude<JSONSchema, boolean>, boolean][];
|
] satisfies [string, Exclude<JSONSchema, boolean>, boolean][];
|
||||||
|
|
||||||
for (const [pointer, schema, output] of examples) {
|
for (const [pointer, schema, output] of examples) {
|
||||||
expect(utils.isRequired(new Draft2019(schema), pointer, schema), `${pointer} `).toBe(
|
expect(utils.isRequired(new Draft2019(schema), pointer, schema)).toBe(output);
|
||||||
output,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+1
-25
@@ -1,29 +1,6 @@
|
|||||||
import pkg from "./package.json" with { type: "json" };
|
import pkg from "./package.json" with { type: "json" };
|
||||||
import c from "picocolors";
|
import c from "picocolors";
|
||||||
import { formatNumber } from "bknd/utils";
|
import { formatNumber } from "core/utils";
|
||||||
import * as esbuild from "esbuild";
|
|
||||||
|
|
||||||
const deps = Object.keys(pkg.dependencies);
|
|
||||||
const external = ["jsonv-ts/*", "wrangler", "bknd", "bknd/*", ...deps];
|
|
||||||
|
|
||||||
if (process.env.DEBUG) {
|
|
||||||
const result = await esbuild.build({
|
|
||||||
entryPoints: ["./src/cli/index.ts"],
|
|
||||||
outdir: "./dist/cli",
|
|
||||||
platform: "node",
|
|
||||||
minify: true,
|
|
||||||
format: "esm",
|
|
||||||
metafile: true,
|
|
||||||
bundle: true,
|
|
||||||
external,
|
|
||||||
define: {
|
|
||||||
__isDev: "0",
|
|
||||||
__version: JSON.stringify(pkg.version),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await Bun.write("./dist/cli/metafile-esm.json", JSON.stringify(result.metafile, null, 2));
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await Bun.build({
|
const result = await Bun.build({
|
||||||
entrypoints: ["./src/cli/index.ts"],
|
entrypoints: ["./src/cli/index.ts"],
|
||||||
@@ -31,7 +8,6 @@ const result = await Bun.build({
|
|||||||
outdir: "./dist/cli",
|
outdir: "./dist/cli",
|
||||||
env: "PUBLIC_*",
|
env: "PUBLIC_*",
|
||||||
minify: true,
|
minify: true,
|
||||||
external,
|
|
||||||
define: {
|
define: {
|
||||||
__isDev: "0",
|
__isDev: "0",
|
||||||
__version: JSON.stringify(pkg.version),
|
__version: JSON.stringify(pkg.version),
|
||||||
|
|||||||
+74
-97
@@ -1,7 +1,6 @@
|
|||||||
import { $ } from "bun";
|
import { $ } from "bun";
|
||||||
import * as tsup from "tsup";
|
import * as tsup from "tsup";
|
||||||
import pkg from "./package.json" with { type: "json" };
|
import pkg from "./package.json" with { type: "json" };
|
||||||
import c from "picocolors";
|
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
const watch = args.includes("--watch");
|
const watch = args.includes("--watch");
|
||||||
@@ -10,14 +9,6 @@ const types = args.includes("--types");
|
|||||||
const sourcemap = args.includes("--sourcemap");
|
const sourcemap = args.includes("--sourcemap");
|
||||||
const clean = args.includes("--clean");
|
const clean = args.includes("--clean");
|
||||||
|
|
||||||
// silence tsup
|
|
||||||
const oldConsole = {
|
|
||||||
log: console.log,
|
|
||||||
warn: console.warn,
|
|
||||||
};
|
|
||||||
console.log = () => {};
|
|
||||||
console.warn = () => {};
|
|
||||||
|
|
||||||
const define = {
|
const define = {
|
||||||
__isDev: "0",
|
__isDev: "0",
|
||||||
__version: JSON.stringify(pkg.version),
|
__version: JSON.stringify(pkg.version),
|
||||||
@@ -36,11 +27,11 @@ function buildTypes() {
|
|||||||
Bun.spawn(["bun", "build:types"], {
|
Bun.spawn(["bun", "build:types"], {
|
||||||
stdout: "inherit",
|
stdout: "inherit",
|
||||||
onExit: () => {
|
onExit: () => {
|
||||||
oldConsole.log(c.cyan("[Types]"), c.green("built"));
|
console.info("Types built");
|
||||||
Bun.spawn(["bun", "tsc-alias"], {
|
Bun.spawn(["bun", "tsc-alias"], {
|
||||||
stdout: "inherit",
|
stdout: "inherit",
|
||||||
onExit: () => {
|
onExit: () => {
|
||||||
oldConsole.log(c.cyan("[Types]"), c.green("aliased"));
|
console.info("Types aliased");
|
||||||
types_running = false;
|
types_running = false;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -48,10 +39,6 @@ function buildTypes() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (types && !watch) {
|
|
||||||
buildTypes();
|
|
||||||
}
|
|
||||||
|
|
||||||
let watcher_timeout: any;
|
let watcher_timeout: any;
|
||||||
function delayTypes() {
|
function delayTypes() {
|
||||||
if (!watch || !types) return;
|
if (!watch || !types) return;
|
||||||
@@ -61,25 +48,32 @@ function delayTypes() {
|
|||||||
watcher_timeout = setTimeout(buildTypes, 1000);
|
watcher_timeout = setTimeout(buildTypes, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dependencies = Object.keys(pkg.dependencies);
|
if (types && !watch) {
|
||||||
|
buildTypes();
|
||||||
|
}
|
||||||
|
|
||||||
|
function banner(title: string) {
|
||||||
|
console.info("");
|
||||||
|
console.info("=".repeat(40));
|
||||||
|
console.info(title.toUpperCase());
|
||||||
|
console.info("-".repeat(40));
|
||||||
|
}
|
||||||
|
|
||||||
// collection of always-external packages
|
// collection of always-external packages
|
||||||
const external = [
|
const external = [
|
||||||
...dependencies,
|
|
||||||
"bun:test",
|
"bun:test",
|
||||||
"node:test",
|
"node:test",
|
||||||
"node:assert/strict",
|
"node:assert/strict",
|
||||||
"@libsql/client",
|
"@libsql/client",
|
||||||
"bknd",
|
"bknd",
|
||||||
/^bknd\/.*/,
|
/^bknd\/.*/,
|
||||||
"jsonv-ts",
|
|
||||||
/^jsonv-ts\/.*/,
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Building backend and general API
|
* Building backend and general API
|
||||||
*/
|
*/
|
||||||
async function buildApi() {
|
async function buildApi() {
|
||||||
|
banner("Building API");
|
||||||
await tsup.build({
|
await tsup.build({
|
||||||
minify,
|
minify,
|
||||||
sourcemap,
|
sourcemap,
|
||||||
@@ -89,16 +83,15 @@ async function buildApi() {
|
|||||||
outDir: "dist",
|
outDir: "dist",
|
||||||
external: [...external],
|
external: [...external],
|
||||||
metafile: true,
|
metafile: true,
|
||||||
target: "esnext",
|
|
||||||
platform: "browser",
|
platform: "browser",
|
||||||
format: ["esm"],
|
format: ["esm"],
|
||||||
splitting: false,
|
splitting: false,
|
||||||
|
treeshake: true,
|
||||||
loader: {
|
loader: {
|
||||||
".svg": "dataurl",
|
".svg": "dataurl",
|
||||||
},
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
delayTypes();
|
delayTypes();
|
||||||
oldConsole.log(c.cyan("[API]"), c.green("built"));
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -142,6 +135,7 @@ async function buildUi() {
|
|||||||
},
|
},
|
||||||
} satisfies tsup.Options;
|
} satisfies tsup.Options;
|
||||||
|
|
||||||
|
banner("Building UI");
|
||||||
await tsup.build({
|
await tsup.build({
|
||||||
...base,
|
...base,
|
||||||
entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"],
|
entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"],
|
||||||
@@ -149,10 +143,10 @@ async function buildUi() {
|
|||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await rewriteClient("./dist/ui/index.js");
|
await rewriteClient("./dist/ui/index.js");
|
||||||
delayTypes();
|
delayTypes();
|
||||||
oldConsole.log(c.cyan("[UI]"), c.green("built"));
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
banner("Building Client");
|
||||||
await tsup.build({
|
await tsup.build({
|
||||||
...base,
|
...base,
|
||||||
entry: ["src/ui/client/index.ts"],
|
entry: ["src/ui/client/index.ts"],
|
||||||
@@ -160,7 +154,6 @@ async function buildUi() {
|
|||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await rewriteClient("./dist/ui/client/index.js");
|
await rewriteClient("./dist/ui/client/index.js");
|
||||||
delayTypes();
|
delayTypes();
|
||||||
oldConsole.log(c.cyan("[UI]"), "Client", c.green("built"));
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -171,6 +164,7 @@ async function buildUi() {
|
|||||||
* - ui/client is external, and after built replaced with "bknd/client"
|
* - ui/client is external, and after built replaced with "bknd/client"
|
||||||
*/
|
*/
|
||||||
async function buildUiElements() {
|
async function buildUiElements() {
|
||||||
|
banner("Building UI Elements");
|
||||||
await tsup.build({
|
await tsup.build({
|
||||||
minify,
|
minify,
|
||||||
sourcemap,
|
sourcemap,
|
||||||
@@ -204,7 +198,6 @@ async function buildUiElements() {
|
|||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await rewriteClient("./dist/ui/elements/index.js");
|
await rewriteClient("./dist/ui/elements/index.js");
|
||||||
delayTypes();
|
delayTypes();
|
||||||
oldConsole.log(c.cyan("[UI]"), "Elements", c.green("built"));
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -225,7 +218,6 @@ function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsu
|
|||||||
splitting: false,
|
splitting: false,
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
delayTypes();
|
delayTypes();
|
||||||
oldConsole.log(c.cyan("[Adapter]"), adapter || "base", c.green("built"));
|
|
||||||
},
|
},
|
||||||
...overrides,
|
...overrides,
|
||||||
define: {
|
define: {
|
||||||
@@ -244,80 +236,65 @@ function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsu
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function buildAdapters() {
|
async function buildAdapters() {
|
||||||
await Promise.all([
|
banner("Building Adapters");
|
||||||
// base adapter handles
|
// base adapter handles
|
||||||
tsup.build({
|
await tsup.build({
|
||||||
...baseConfig(""),
|
...baseConfig(""),
|
||||||
target: "esnext",
|
entry: ["src/adapter/index.ts"],
|
||||||
platform: "neutral",
|
outDir: "dist/adapter",
|
||||||
entry: ["src/adapter/index.ts"],
|
});
|
||||||
outDir: "dist/adapter",
|
|
||||||
// only way to keep @vite-ignore comments
|
|
||||||
minify: false,
|
|
||||||
}),
|
|
||||||
|
|
||||||
// specific adatpers
|
// specific adatpers
|
||||||
tsup.build(baseConfig("react-router")),
|
await tsup.build(baseConfig("react-router"));
|
||||||
tsup.build(
|
await tsup.build(
|
||||||
baseConfig("bun", {
|
baseConfig("bun", {
|
||||||
external: [/^bun\:.*/],
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
tsup.build(baseConfig("astro")),
|
|
||||||
tsup.build(baseConfig("aws")),
|
|
||||||
tsup.build(
|
|
||||||
baseConfig("cloudflare", {
|
|
||||||
external: ["wrangler", "node:process"],
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
tsup.build(
|
|
||||||
baseConfig("cloudflare/proxy", {
|
|
||||||
target: "esnext",
|
|
||||||
entry: ["src/adapter/cloudflare/proxy.ts"],
|
|
||||||
outDir: "dist/adapter/cloudflare",
|
|
||||||
metafile: false,
|
|
||||||
external: [/bknd/, "wrangler", "node:process"],
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
|
|
||||||
tsup.build({
|
|
||||||
...baseConfig("vite"),
|
|
||||||
platform: "node",
|
|
||||||
}),
|
|
||||||
|
|
||||||
tsup.build({
|
|
||||||
...baseConfig("nextjs"),
|
|
||||||
platform: "node",
|
|
||||||
}),
|
|
||||||
|
|
||||||
tsup.build({
|
|
||||||
...baseConfig("node"),
|
|
||||||
platform: "node",
|
|
||||||
}),
|
|
||||||
|
|
||||||
tsup.build({
|
|
||||||
...baseConfig("sqlite/edge"),
|
|
||||||
entry: ["src/adapter/sqlite/edge.ts"],
|
|
||||||
outDir: "dist/adapter/sqlite",
|
|
||||||
metafile: false,
|
|
||||||
}),
|
|
||||||
|
|
||||||
tsup.build({
|
|
||||||
...baseConfig("sqlite/node"),
|
|
||||||
entry: ["src/adapter/sqlite/node.ts"],
|
|
||||||
outDir: "dist/adapter/sqlite",
|
|
||||||
platform: "node",
|
|
||||||
metafile: false,
|
|
||||||
}),
|
|
||||||
|
|
||||||
tsup.build({
|
|
||||||
...baseConfig("sqlite/bun"),
|
|
||||||
entry: ["src/adapter/sqlite/bun.ts"],
|
|
||||||
outDir: "dist/adapter/sqlite",
|
|
||||||
metafile: false,
|
|
||||||
external: [/^bun\:.*/],
|
external: [/^bun\:.*/],
|
||||||
}),
|
}),
|
||||||
]);
|
);
|
||||||
|
await tsup.build(baseConfig("astro"));
|
||||||
|
await tsup.build(baseConfig("aws"));
|
||||||
|
await tsup.build(baseConfig("cloudflare"));
|
||||||
|
|
||||||
|
await tsup.build({
|
||||||
|
...baseConfig("vite"),
|
||||||
|
platform: "node",
|
||||||
|
});
|
||||||
|
|
||||||
|
await tsup.build({
|
||||||
|
...baseConfig("nextjs"),
|
||||||
|
platform: "node",
|
||||||
|
});
|
||||||
|
|
||||||
|
await tsup.build({
|
||||||
|
...baseConfig("node"),
|
||||||
|
platform: "node",
|
||||||
|
});
|
||||||
|
|
||||||
|
await tsup.build({
|
||||||
|
...baseConfig("sqlite/edge"),
|
||||||
|
entry: ["src/adapter/sqlite/edge.ts"],
|
||||||
|
outDir: "dist/adapter/sqlite",
|
||||||
|
metafile: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await tsup.build({
|
||||||
|
...baseConfig("sqlite/node"),
|
||||||
|
entry: ["src/adapter/sqlite/node.ts"],
|
||||||
|
outDir: "dist/adapter/sqlite",
|
||||||
|
platform: "node",
|
||||||
|
metafile: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await tsup.build({
|
||||||
|
...baseConfig("sqlite/bun"),
|
||||||
|
entry: ["src/adapter/sqlite/bun.ts"],
|
||||||
|
outDir: "dist/adapter/sqlite",
|
||||||
|
metafile: false,
|
||||||
|
external: [/^bun\:.*/],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all([buildApi(), buildUi(), buildUiElements(), buildAdapters()]);
|
await buildApi();
|
||||||
|
await buildUi();
|
||||||
|
await buildUiElements();
|
||||||
|
await buildAdapters();
|
||||||
|
|||||||
@@ -3,4 +3,3 @@
|
|||||||
|
|
||||||
[test]
|
[test]
|
||||||
coverageSkipTestFiles = true
|
coverageSkipTestFiles = true
|
||||||
console.depth = 10
|
|
||||||
+6
-6
@@ -17,7 +17,7 @@ async function run(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Read from stdout
|
// Read from stdout
|
||||||
const reader = (proc.stdout as ReadableStream).getReader();
|
const reader = proc.stdout.getReader();
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
// Function to read chunks
|
// Function to read chunks
|
||||||
@@ -30,7 +30,7 @@ async function run(
|
|||||||
|
|
||||||
const text = decoder.decode(value);
|
const text = decoder.decode(value);
|
||||||
if (!resolveCalled) {
|
if (!resolveCalled) {
|
||||||
console.info(c.dim(text.replace(/\n$/, "")));
|
console.log(c.dim(text.replace(/\n$/, "")));
|
||||||
}
|
}
|
||||||
onChunk(
|
onChunk(
|
||||||
text,
|
text,
|
||||||
@@ -189,21 +189,21 @@ const adapters = {
|
|||||||
|
|
||||||
async function testAdapter(name: keyof typeof adapters) {
|
async function testAdapter(name: keyof typeof adapters) {
|
||||||
const config = adapters[name];
|
const config = adapters[name];
|
||||||
console.info("adapter", c.cyan(name));
|
console.log("adapter", c.cyan(name));
|
||||||
await config.clean();
|
await config.clean();
|
||||||
|
|
||||||
const { proc, data } = await config.start();
|
const { proc, data } = await config.start();
|
||||||
console.info("proc:", proc.pid, "data:", c.cyan(data));
|
console.log("proc:", proc.pid, "data:", c.cyan(data));
|
||||||
//proc.kill();process.exit(0);
|
//proc.kill();process.exit(0);
|
||||||
|
|
||||||
const add_env = "env" in config && config.env ? config.env : "";
|
const add_env = "env" in config && config.env ? config.env : "";
|
||||||
await $`TEST_URL=${data} TEST_ADAPTER=${name} ${add_env} bun run test:e2e`;
|
await $`TEST_URL=${data} TEST_ADAPTER=${name} ${add_env} bun run test:e2e`;
|
||||||
console.info("DONE!");
|
console.log("DONE!");
|
||||||
|
|
||||||
while (!proc.killed) {
|
while (!proc.killed) {
|
||||||
proc.kill("SIGINT");
|
proc.kill("SIGINT");
|
||||||
await Bun.sleep(250);
|
await Bun.sleep(250);
|
||||||
console.info("Waiting for process to exit...");
|
console.log("Waiting for process to exit...");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import { createApp } from "bknd/adapter/bun";
|
|
||||||
|
|
||||||
async function generate() {
|
|
||||||
console.info("Generating MCP documentation...");
|
|
||||||
const app = await createApp({
|
|
||||||
config: {
|
|
||||||
server: {
|
|
||||||
mcp: {
|
|
||||||
enabled: true,
|
|
||||||
path: "/mcp",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
auth: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
media: {
|
|
||||||
enabled: true,
|
|
||||||
adapter: {
|
|
||||||
type: "local",
|
|
||||||
config: {
|
|
||||||
path: "./",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await app.build();
|
|
||||||
|
|
||||||
const res = await app.server.request("/mcp?explain=1");
|
|
||||||
const { tools, resources } = await res.json();
|
|
||||||
await Bun.write("../docs/mcp.json", JSON.stringify({ tools, resources }, null, 2));
|
|
||||||
|
|
||||||
console.info("MCP documentation generated.");
|
|
||||||
}
|
|
||||||
|
|
||||||
void generate();
|
|
||||||
+14
-23
@@ -3,7 +3,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"bin": "./dist/cli/index.js",
|
"bin": "./dist/cli/index.js",
|
||||||
"version": "0.18.0",
|
"version": "0.16.0-rc.0",
|
||||||
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
|
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
|
||||||
"homepage": "https://bknd.io",
|
"homepage": "https://bknd.io",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -13,9 +13,9 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/bknd-io/bknd/issues"
|
"url": "https://github.com/bknd-io/bknd/issues"
|
||||||
},
|
},
|
||||||
"packageManager": "bun@1.2.22",
|
"packageManager": "bun@1.2.19",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.13"
|
"node": ">=22"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "BKND_CLI_LOG_LEVEL=debug vite",
|
"dev": "BKND_CLI_LOG_LEVEL=debug vite",
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly && tsc-alias",
|
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly && tsc-alias",
|
||||||
"updater": "bun x npm-check-updates -ui",
|
"updater": "bun x npm-check-updates -ui",
|
||||||
"cli": "LOCAL=1 bun src/cli/index.ts",
|
"cli": "LOCAL=1 bun src/cli/index.ts",
|
||||||
"prepublishOnly": "bun run types && bun run test && bun run test:node && NODE_NO_WARNINGS=1 VITE_DB_URL=:memory: bun run test:e2e && bun run build:all && cp ../README.md ./",
|
"prepublishOnly": "bun run types && bun run test && bun run test:node && bun run test:e2e && bun run build:all && cp ../README.md ./",
|
||||||
"postpublish": "rm -f README.md",
|
"postpublish": "rm -f README.md",
|
||||||
"test": "ALL_TESTS=1 bun test --bail",
|
"test": "ALL_TESTS=1 bun test --bail",
|
||||||
"test:all": "bun run test && bun run test:node",
|
"test:all": "bun run test && bun run test:node",
|
||||||
@@ -40,11 +40,10 @@
|
|||||||
"test:coverage": "ALL_TESTS=1 bun test --bail --coverage",
|
"test:coverage": "ALL_TESTS=1 bun test --bail --coverage",
|
||||||
"test:vitest:coverage": "vitest run --coverage",
|
"test:vitest:coverage": "vitest run --coverage",
|
||||||
"test:e2e": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
"test:e2e:adapters": "NODE_NO_WARNINGS=1 bun run e2e/adapters.ts",
|
"test:e2e:adapters": "bun run e2e/adapters.ts",
|
||||||
"test:e2e:ui": "VITE_DB_URL=:memory: playwright test --ui",
|
"test:e2e:ui": "playwright test --ui",
|
||||||
"test:e2e:debug": "VITE_DB_URL=:memory: playwright test --debug",
|
"test:e2e:debug": "playwright test --debug",
|
||||||
"test:e2e:report": "VITE_DB_URL=:memory: playwright show-report",
|
"test:e2e:report": "playwright show-report"
|
||||||
"docs:build-assets": "bun internal/docs.build-assets.ts"
|
|
||||||
},
|
},
|
||||||
"license": "FSL-1.1-MIT",
|
"license": "FSL-1.1-MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -65,23 +64,20 @@
|
|||||||
"hono": "4.8.3",
|
"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",
|
||||||
"jsonv-ts": "0.8.4",
|
"kysely": "^0.27.6",
|
||||||
"kysely": "0.27.6",
|
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"oauth4webapi": "^2.11.1",
|
"oauth4webapi": "^2.11.1",
|
||||||
"object-path-immutable": "^4.1.2",
|
"object-path-immutable": "^4.1.2",
|
||||||
"radix-ui": "^1.1.3",
|
"radix-ui": "^1.1.3",
|
||||||
"picocolors": "^1.1.1",
|
|
||||||
"swr": "^2.3.3"
|
"swr": "^2.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.758.0",
|
"@aws-sdk/client-s3": "^3.758.0",
|
||||||
"@bluwy/giget-core": "^0.1.2",
|
"@bluwy/giget-core": "^0.1.2",
|
||||||
"@clack/prompts": "^0.11.0",
|
"@cloudflare/vitest-pool-workers": "^0.8.38",
|
||||||
"@cloudflare/vitest-pool-workers": "^0.9.3",
|
|
||||||
"@cloudflare/workers-types": "^4.20250606.0",
|
"@cloudflare/workers-types": "^4.20250606.0",
|
||||||
"@dagrejs/dagre": "^1.1.4",
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
"@hono/vite-dev-server": "^0.21.0",
|
"@hono/vite-dev-server": "^0.19.1",
|
||||||
"@hookform/resolvers": "^4.1.3",
|
"@hookform/resolvers": "^4.1.3",
|
||||||
"@libsql/client": "^0.15.9",
|
"@libsql/client": "^0.15.9",
|
||||||
"@mantine/modals": "^7.17.1",
|
"@mantine/modals": "^7.17.1",
|
||||||
@@ -104,11 +100,13 @@
|
|||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"jotai": "^2.12.2",
|
"jotai": "^2.12.2",
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.0.0",
|
||||||
|
"jsonv-ts": "^0.3.2",
|
||||||
"kysely-d1": "^0.3.0",
|
"kysely-d1": "^0.3.0",
|
||||||
"kysely-generic-sqlite": "^1.2.1",
|
"kysely-generic-sqlite": "^1.2.1",
|
||||||
"libsql-stateless-easy": "^1.8.0",
|
"libsql-stateless-easy": "^1.8.0",
|
||||||
"open": "^10.1.0",
|
"open": "^10.1.0",
|
||||||
"openapi-types": "^12.1.3",
|
"openapi-types": "^12.1.3",
|
||||||
|
"picocolors": "^1.1.1",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"postcss-preset-mantine": "^1.17.0",
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
@@ -130,9 +128,7 @@
|
|||||||
"vite-plugin-circular-dependency": "^0.5.0",
|
"vite-plugin-circular-dependency": "^0.5.0",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
"vitest": "^3.0.9",
|
"vitest": "^3.0.9",
|
||||||
"wouter": "^3.6.0",
|
"wouter": "^3.6.0"
|
||||||
"wrangler": "^4.37.1",
|
|
||||||
"miniflare": "^4.20250913.0"
|
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@hono/node-server": "^1.14.3"
|
"@hono/node-server": "^1.14.3"
|
||||||
@@ -199,11 +195,6 @@
|
|||||||
"import": "./dist/adapter/cloudflare/index.js",
|
"import": "./dist/adapter/cloudflare/index.js",
|
||||||
"require": "./dist/adapter/cloudflare/index.js"
|
"require": "./dist/adapter/cloudflare/index.js"
|
||||||
},
|
},
|
||||||
"./adapter/cloudflare/proxy": {
|
|
||||||
"types": "./dist/types/adapter/cloudflare/proxy.d.ts",
|
|
||||||
"import": "./dist/adapter/cloudflare/proxy.js",
|
|
||||||
"require": "./dist/adapter/cloudflare/proxy.js"
|
|
||||||
},
|
|
||||||
"./adapter": {
|
"./adapter": {
|
||||||
"types": "./dist/types/adapter/index.d.ts",
|
"types": "./dist/types/adapter/index.d.ts",
|
||||||
"import": "./dist/adapter/index.js"
|
"import": "./dist/adapter/index.js"
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@ import { DataApi, type DataApiOptions } from "data/api/DataApi";
|
|||||||
import { decode } from "hono/jwt";
|
import { decode } from "hono/jwt";
|
||||||
import { MediaApi, type MediaApiOptions } from "media/api/MediaApi";
|
import { MediaApi, type MediaApiOptions } from "media/api/MediaApi";
|
||||||
import { SystemApi } from "modules/SystemApi";
|
import { SystemApi } from "modules/SystemApi";
|
||||||
import { omitKeys } from "bknd/utils";
|
import { omitKeys } from "core/utils";
|
||||||
import type { BaseModuleApiOptions } from "modules";
|
import type { BaseModuleApiOptions } from "modules";
|
||||||
|
|
||||||
export type TApiUser = SafeUser;
|
export type TApiUser = SafeUser;
|
||||||
@@ -43,7 +43,7 @@ export type ApiOptions = {
|
|||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
token?: string;
|
token?: string;
|
||||||
user?: TApiUser | null;
|
user?: TApiUser;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
request: Request;
|
request: Request;
|
||||||
|
|||||||
+22
-83
@@ -1,22 +1,21 @@
|
|||||||
import type { CreateUserPayload } from "auth/AppAuth";
|
import type { CreateUserPayload } from "auth/AppAuth";
|
||||||
import { $console, McpClient } from "bknd/utils";
|
import { $console } from "core/utils";
|
||||||
import { Event } from "core/events";
|
import { Event } from "core/events";
|
||||||
import type { em as prototypeEm } from "data/prototype";
|
import type { em as prototypeEm } from "data/prototype";
|
||||||
import { Connection } from "data/connection/Connection";
|
import { Connection } from "data/connection/Connection";
|
||||||
import type { Hono } from "hono";
|
import type { Hono } from "hono";
|
||||||
import {
|
import {
|
||||||
type InitialModuleConfigs,
|
|
||||||
type ModuleConfigs,
|
|
||||||
type Modules,
|
|
||||||
ModuleManager,
|
ModuleManager,
|
||||||
|
type InitialModuleConfigs,
|
||||||
type ModuleBuildContext,
|
type ModuleBuildContext,
|
||||||
|
type ModuleConfigs,
|
||||||
type ModuleManagerOptions,
|
type ModuleManagerOptions,
|
||||||
|
type Modules,
|
||||||
} from "modules/ModuleManager";
|
} from "modules/ModuleManager";
|
||||||
import { DbModuleManager } from "modules/db/DbModuleManager";
|
|
||||||
import * as SystemPermissions from "modules/permissions";
|
import * as SystemPermissions from "modules/permissions";
|
||||||
import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
|
import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
|
||||||
import { SystemController } from "modules/server/SystemController";
|
import { SystemController } from "modules/server/SystemController";
|
||||||
import type { MaybePromise, PartialRec } from "core/types";
|
import type { MaybePromise } from "core/types";
|
||||||
import type { ServerEnv } from "modules/Controller";
|
import type { ServerEnv } from "modules/Controller";
|
||||||
import type { IEmailDriver, ICacheDriver } from "core/drivers";
|
import type { IEmailDriver, ICacheDriver } from "core/drivers";
|
||||||
|
|
||||||
@@ -24,34 +23,13 @@ import type { IEmailDriver, ICacheDriver } from "core/drivers";
|
|||||||
import { Api, type ApiOptions } from "Api";
|
import { Api, type ApiOptions } from "Api";
|
||||||
|
|
||||||
export type AppPluginConfig = {
|
export type AppPluginConfig = {
|
||||||
/**
|
|
||||||
* The name of the plugin.
|
|
||||||
*/
|
|
||||||
name: string;
|
name: string;
|
||||||
/**
|
|
||||||
* The schema of the plugin.
|
|
||||||
*/
|
|
||||||
schema?: () => MaybePromise<ReturnType<typeof prototypeEm> | void>;
|
schema?: () => MaybePromise<ReturnType<typeof prototypeEm> | void>;
|
||||||
/**
|
|
||||||
* Called before the app is built.
|
|
||||||
*/
|
|
||||||
beforeBuild?: () => MaybePromise<void>;
|
beforeBuild?: () => MaybePromise<void>;
|
||||||
/**
|
|
||||||
* Called after the app is built.
|
|
||||||
*/
|
|
||||||
onBuilt?: () => MaybePromise<void>;
|
onBuilt?: () => MaybePromise<void>;
|
||||||
/**
|
|
||||||
* Called when the server is initialized.
|
|
||||||
*/
|
|
||||||
onServerInit?: (server: Hono<ServerEnv>) => MaybePromise<void>;
|
onServerInit?: (server: Hono<ServerEnv>) => MaybePromise<void>;
|
||||||
/**
|
|
||||||
* Called when the app is booted.
|
|
||||||
*/
|
|
||||||
onBoot?: () => MaybePromise<void>;
|
|
||||||
/**
|
|
||||||
* Called when the app is first booted.
|
|
||||||
*/
|
|
||||||
onFirstBoot?: () => MaybePromise<void>;
|
onFirstBoot?: () => MaybePromise<void>;
|
||||||
|
onBoot?: () => MaybePromise<void>;
|
||||||
};
|
};
|
||||||
export type AppPlugin = (app: App) => AppPluginConfig;
|
export type AppPlugin = (app: App) => AppPluginConfig;
|
||||||
|
|
||||||
@@ -62,9 +40,6 @@ export class AppConfigUpdatedEvent extends AppEvent<{
|
|||||||
}> {
|
}> {
|
||||||
static override slug = "app-config-updated";
|
static override slug = "app-config-updated";
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* @type {Event<{ app: App }>}
|
|
||||||
*/
|
|
||||||
export class AppBuiltEvent extends AppEvent {
|
export class AppBuiltEvent extends AppEvent {
|
||||||
static override slug = "app-built";
|
static override slug = "app-built";
|
||||||
}
|
}
|
||||||
@@ -94,23 +69,17 @@ export type AppOptions = {
|
|||||||
email?: IEmailDriver;
|
email?: IEmailDriver;
|
||||||
cache?: ICacheDriver;
|
cache?: ICacheDriver;
|
||||||
};
|
};
|
||||||
mode?: "db" | "code";
|
|
||||||
readonly?: boolean;
|
|
||||||
};
|
};
|
||||||
export type CreateAppConfig = {
|
export type CreateAppConfig = {
|
||||||
connection?: Connection | { url: string };
|
connection?: Connection | { url: string };
|
||||||
config?: PartialRec<ModuleConfigs>;
|
initialConfig?: InitialModuleConfigs;
|
||||||
options?: AppOptions;
|
options?: AppOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AppConfig = { version: number } & ModuleConfigs;
|
export type AppConfig = InitialModuleConfigs;
|
||||||
export type LocalApiOptions = Request | ApiOptions;
|
export type LocalApiOptions = Request | ApiOptions;
|
||||||
|
|
||||||
export class App<
|
export class App<C extends Connection = Connection, Options extends AppOptions = AppOptions> {
|
||||||
C extends Connection = Connection,
|
|
||||||
Config extends PartialRec<ModuleConfigs> = PartialRec<ModuleConfigs>,
|
|
||||||
Options extends AppOptions = AppOptions,
|
|
||||||
> {
|
|
||||||
static readonly Events = AppEvents;
|
static readonly Events = AppEvents;
|
||||||
|
|
||||||
modules: ModuleManager;
|
modules: ModuleManager;
|
||||||
@@ -121,12 +90,11 @@ export class App<
|
|||||||
|
|
||||||
private trigger_first_boot = false;
|
private trigger_first_boot = false;
|
||||||
private _building: boolean = false;
|
private _building: boolean = false;
|
||||||
private _systemController: SystemController | null = null;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public connection: C,
|
public connection: C,
|
||||||
_config?: Config,
|
_initialConfig?: InitialModuleConfigs,
|
||||||
public options?: Options,
|
private options?: Options,
|
||||||
) {
|
) {
|
||||||
this.drivers = options?.drivers ?? {};
|
this.drivers = options?.drivers ?? {};
|
||||||
|
|
||||||
@@ -138,13 +106,9 @@ export class App<
|
|||||||
this.plugins.set(config.name, config);
|
this.plugins.set(config.name, config);
|
||||||
}
|
}
|
||||||
this.runPlugins("onBoot");
|
this.runPlugins("onBoot");
|
||||||
|
this.modules = new ModuleManager(connection, {
|
||||||
// use db manager by default
|
|
||||||
const Manager = this.mode === "db" ? DbModuleManager : ModuleManager;
|
|
||||||
|
|
||||||
this.modules = new Manager(connection, {
|
|
||||||
...(options?.manager ?? {}),
|
...(options?.manager ?? {}),
|
||||||
initial: _config,
|
initial: _initialConfig,
|
||||||
onUpdated: this.onUpdated.bind(this),
|
onUpdated: this.onUpdated.bind(this),
|
||||||
onFirstBoot: this.onFirstBoot.bind(this),
|
onFirstBoot: this.onFirstBoot.bind(this),
|
||||||
onServerInit: this.onServerInit.bind(this),
|
onServerInit: this.onServerInit.bind(this),
|
||||||
@@ -153,14 +117,6 @@ export class App<
|
|||||||
this.modules.ctx().emgr.registerEvents(AppEvents);
|
this.modules.ctx().emgr.registerEvents(AppEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
get mode() {
|
|
||||||
return this.options?.mode ?? "db";
|
|
||||||
}
|
|
||||||
|
|
||||||
isReadOnly() {
|
|
||||||
return Boolean(this.mode === "code" || this.options?.readonly);
|
|
||||||
}
|
|
||||||
|
|
||||||
get emgr() {
|
get emgr() {
|
||||||
return this.modules.ctx().emgr;
|
return this.modules.ctx().emgr;
|
||||||
}
|
}
|
||||||
@@ -191,7 +147,7 @@ export class App<
|
|||||||
return results as any;
|
return results as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
async build(options?: { sync?: boolean; forceBuild?: boolean; [key: string]: any }) {
|
async build(options?: { sync?: boolean; fetch?: boolean; forceBuild?: boolean }) {
|
||||||
// prevent multiple concurrent builds
|
// prevent multiple concurrent builds
|
||||||
if (this._building) {
|
if (this._building) {
|
||||||
while (this._building) {
|
while (this._building) {
|
||||||
@@ -204,14 +160,13 @@ export class App<
|
|||||||
this._building = true;
|
this._building = true;
|
||||||
|
|
||||||
if (options?.sync) this.modules.ctx().flags.sync_required = true;
|
if (options?.sync) this.modules.ctx().flags.sync_required = true;
|
||||||
await this.modules.build();
|
await this.modules.build({ fetch: options?.fetch });
|
||||||
|
|
||||||
const { guard } = this.modules.ctx();
|
const { guard, server } = this.modules.ctx();
|
||||||
|
|
||||||
// load system controller
|
// load system controller
|
||||||
guard.registerPermissions(Object.values(SystemPermissions));
|
guard.registerPermissions(Object.values(SystemPermissions));
|
||||||
this._systemController = new SystemController(this);
|
server.route("/api/system", new SystemController(this).getController());
|
||||||
this._systemController.register(this);
|
|
||||||
|
|
||||||
// emit built event
|
// emit built event
|
||||||
$console.log("App built");
|
$console.log("App built");
|
||||||
@@ -231,6 +186,10 @@ export class App<
|
|||||||
this._building = false;
|
this._building = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mutateConfig<Module extends keyof Modules>(module: Module) {
|
||||||
|
return this.modules.mutateConfigSafe(module);
|
||||||
|
}
|
||||||
|
|
||||||
get server() {
|
get server() {
|
||||||
return this.modules.server;
|
return this.modules.server;
|
||||||
}
|
}
|
||||||
@@ -239,15 +198,7 @@ export class App<
|
|||||||
return this.modules.ctx().em;
|
return this.modules.ctx().em;
|
||||||
}
|
}
|
||||||
|
|
||||||
get mcp() {
|
|
||||||
return this._systemController?._mcpServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
get fetch(): Hono["fetch"] {
|
get fetch(): Hono["fetch"] {
|
||||||
if (!this.isBuilt()) {
|
|
||||||
throw new Error("App is not built yet, run build() first");
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.server.fetch as any;
|
return this.server.fetch as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,18 +256,6 @@ export class App<
|
|||||||
return new Api({ host: "http://localhost", ...(options ?? {}), fetcher });
|
return new Api({ host: "http://localhost", ...(options ?? {}), fetcher });
|
||||||
}
|
}
|
||||||
|
|
||||||
getMcpClient() {
|
|
||||||
const config = this.modules.get("server").config.mcp;
|
|
||||||
if (!config.enabled) {
|
|
||||||
throw new Error("MCP is not enabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new McpClient({
|
|
||||||
url: "http://localhost" + config.path,
|
|
||||||
fetch: this.server.request,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async onUpdated<Module extends keyof Modules>(module: Module, config: ModuleConfigs[Module]) {
|
async onUpdated<Module extends keyof Modules>(module: Module, config: ModuleConfigs[Module]) {
|
||||||
// if the EventManager was disabled, we assume we shouldn't
|
// if the EventManager was disabled, we assume we shouldn't
|
||||||
// respond to events, such as "onUpdated".
|
// respond to events, such as "onUpdated".
|
||||||
@@ -393,5 +332,5 @@ export function createApp(config: CreateAppConfig = {}) {
|
|||||||
throw new Error("Invalid connection");
|
throw new Error("Invalid connection");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new App(config.connection, config.config, config.options);
|
return new App(config.connection, config.initialConfig, config.options);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { TestRunner } from "core/test";
|
import type { TestRunner } from "core/test";
|
||||||
import type { BkndConfig, DefaultArgs } from "./index";
|
import type { BkndConfig, DefaultArgs, FrameworkOptions, RuntimeOptions } from "./index";
|
||||||
import type { App } from "App";
|
import type { App } from "App";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
export function adapterTestSuite<
|
export function adapterTestSuite<
|
||||||
Config extends BkndConfig = BkndConfig,
|
Config extends BkndConfig = BkndConfig,
|
||||||
@@ -14,17 +13,24 @@ export function adapterTestSuite<
|
|||||||
label = "app",
|
label = "app",
|
||||||
overrides = {},
|
overrides = {},
|
||||||
}: {
|
}: {
|
||||||
makeApp: (config: Config, args?: Args) => Promise<App>;
|
makeApp: (
|
||||||
makeHandler?: (config?: Config, args?: Args) => (request: Request) => Promise<Response>;
|
config: Config,
|
||||||
|
args?: Args,
|
||||||
|
opts?: RuntimeOptions | FrameworkOptions,
|
||||||
|
) => Promise<App>;
|
||||||
|
makeHandler?: (
|
||||||
|
config?: Config,
|
||||||
|
args?: Args,
|
||||||
|
opts?: RuntimeOptions | FrameworkOptions,
|
||||||
|
) => (request: Request) => Promise<Response>;
|
||||||
label?: string;
|
label?: string;
|
||||||
overrides?: {
|
overrides?: {
|
||||||
dbUrl?: string;
|
dbUrl?: string;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const { test, expect, mock, beforeAll, afterAll } = testRunner;
|
const { test, expect, mock } = testRunner;
|
||||||
beforeAll(() => disableConsoleLog());
|
const id = crypto.randomUUID();
|
||||||
afterAll(() => enableConsoleLog());
|
|
||||||
|
|
||||||
test(`creates ${label}`, async () => {
|
test(`creates ${label}`, async () => {
|
||||||
const beforeBuild = mock(async () => null) as any;
|
const beforeBuild = mock(async () => null) as any;
|
||||||
@@ -33,7 +39,7 @@ export function adapterTestSuite<
|
|||||||
const config = {
|
const config = {
|
||||||
app: (env) => ({
|
app: (env) => ({
|
||||||
connection: { url: env.url },
|
connection: { url: env.url },
|
||||||
config: {
|
initialConfig: {
|
||||||
server: { cors: { origin: env.origin } },
|
server: { cors: { origin: env.origin } },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -47,10 +53,11 @@ export function adapterTestSuite<
|
|||||||
url: overrides.dbUrl ?? ":memory:",
|
url: overrides.dbUrl ?? ":memory:",
|
||||||
origin: "localhost",
|
origin: "localhost",
|
||||||
} as any,
|
} as any,
|
||||||
|
{ id },
|
||||||
);
|
);
|
||||||
expect(app).toBeDefined();
|
expect(app).toBeDefined();
|
||||||
expect(app.toJSON().server.cors.origin).toEqual("localhost");
|
expect(app.toJSON().server.cors.origin).toEqual("localhost");
|
||||||
expect(beforeBuild).toHaveBeenCalledTimes(2);
|
expect(beforeBuild).toHaveBeenCalledTimes(1);
|
||||||
expect(onBuilt).toHaveBeenCalledTimes(1);
|
expect(onBuilt).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,7 +68,7 @@ export function adapterTestSuite<
|
|||||||
return { res, data };
|
return { res, data };
|
||||||
};
|
};
|
||||||
|
|
||||||
/* test.skip("responds with the same app id", async () => {
|
test("responds with the same app id", async () => {
|
||||||
const fetcher = makeHandler(undefined, undefined, { id });
|
const fetcher = makeHandler(undefined, undefined, { id });
|
||||||
|
|
||||||
const { res, data } = await getConfig(fetcher);
|
const { res, data } = await getConfig(fetcher);
|
||||||
@@ -70,14 +77,14 @@ export function adapterTestSuite<
|
|||||||
expect(data.server.cors.origin).toEqual("localhost");
|
expect(data.server.cors.origin).toEqual("localhost");
|
||||||
});
|
});
|
||||||
|
|
||||||
test.skip("creates fresh & responds to api config", async () => {
|
test("creates fresh & responds to api config", async () => {
|
||||||
// set the same id, but force recreate
|
// set the same id, but force recreate
|
||||||
const fetcher = makeHandler(undefined, undefined, { id });
|
const fetcher = makeHandler(undefined, undefined, { id, force: true });
|
||||||
|
|
||||||
const { res, data } = await getConfig(fetcher);
|
const { res, data } = await getConfig(fetcher);
|
||||||
expect(res.ok).toBe(true);
|
expect(res.ok).toBe(true);
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(data.server.cors.origin).toEqual("*");
|
expect(data.server.cors.origin).toEqual("*");
|
||||||
}); */
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ afterAll(enableConsoleLog);
|
|||||||
describe("astro adapter", () => {
|
describe("astro adapter", () => {
|
||||||
adapterTestSuite(bunTestRunner, {
|
adapterTestSuite(bunTestRunner, {
|
||||||
makeApp: astro.getApp,
|
makeApp: astro.getApp,
|
||||||
makeHandler: (c, a) => (request: Request) => astro.serve(c, a)({ request }),
|
makeHandler: (c, a, o) => (request: Request) => astro.serve(c, a, o)({ request }),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter";
|
import { type FrameworkBkndConfig, createFrameworkApp, type FrameworkOptions } from "bknd/adapter";
|
||||||
|
|
||||||
type AstroEnv = NodeJS.ProcessEnv;
|
type AstroEnv = NodeJS.ProcessEnv;
|
||||||
type TAstro = {
|
type TAstro = {
|
||||||
@@ -9,12 +9,17 @@ export type AstroBkndConfig<Env = AstroEnv> = FrameworkBkndConfig<Env>;
|
|||||||
export async function getApp<Env = AstroEnv>(
|
export async function getApp<Env = AstroEnv>(
|
||||||
config: AstroBkndConfig<Env> = {},
|
config: AstroBkndConfig<Env> = {},
|
||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
|
opts: FrameworkOptions = {},
|
||||||
) {
|
) {
|
||||||
return await createFrameworkApp(config, args ?? import.meta.env);
|
return await createFrameworkApp(config, args ?? import.meta.env, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serve<Env = AstroEnv>(config: AstroBkndConfig<Env> = {}, args: Env = {} as Env) {
|
export function serve<Env = AstroEnv>(
|
||||||
|
config: AstroBkndConfig<Env> = {},
|
||||||
|
args: Env = {} as Env,
|
||||||
|
opts?: FrameworkOptions,
|
||||||
|
) {
|
||||||
return async (fnArgs: TAstro) => {
|
return async (fnArgs: TAstro) => {
|
||||||
return (await getApp(config, args)).fetch(fnArgs.request);
|
return (await getApp(config, args, opts)).fetch(fnArgs.request);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { App } from "bknd";
|
import type { App } from "bknd";
|
||||||
import { handle } from "hono/aws-lambda";
|
import { handle } from "hono/aws-lambda";
|
||||||
import { serveStatic } from "@hono/node-server/serve-static";
|
import { serveStatic } from "@hono/node-server/serve-static";
|
||||||
import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter";
|
import { type RuntimeBkndConfig, createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
|
||||||
|
|
||||||
type AwsLambdaEnv = object;
|
type AwsLambdaEnv = object;
|
||||||
export type AwsLambdaBkndConfig<Env extends AwsLambdaEnv = AwsLambdaEnv> =
|
export type AwsLambdaBkndConfig<Env extends AwsLambdaEnv = AwsLambdaEnv> =
|
||||||
@@ -20,6 +20,7 @@ export type AwsLambdaBkndConfig<Env extends AwsLambdaEnv = AwsLambdaEnv> =
|
|||||||
export async function createApp<Env extends AwsLambdaEnv = AwsLambdaEnv>(
|
export async function createApp<Env extends AwsLambdaEnv = AwsLambdaEnv>(
|
||||||
{ adminOptions = false, assets, ...config }: AwsLambdaBkndConfig<Env> = {},
|
{ adminOptions = false, assets, ...config }: AwsLambdaBkndConfig<Env> = {},
|
||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
|
opts?: RuntimeOptions,
|
||||||
): Promise<App> {
|
): Promise<App> {
|
||||||
let additional: Partial<RuntimeBkndConfig> = {
|
let additional: Partial<RuntimeBkndConfig> = {
|
||||||
adminOptions,
|
adminOptions,
|
||||||
@@ -56,15 +57,17 @@ export async function createApp<Env extends AwsLambdaEnv = AwsLambdaEnv>(
|
|||||||
...additional,
|
...additional,
|
||||||
},
|
},
|
||||||
args ?? process.env,
|
args ?? process.env,
|
||||||
|
opts,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serve<Env extends AwsLambdaEnv = AwsLambdaEnv>(
|
export function serve<Env extends AwsLambdaEnv = AwsLambdaEnv>(
|
||||||
config: AwsLambdaBkndConfig<Env> = {},
|
config: AwsLambdaBkndConfig<Env> = {},
|
||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
|
opts?: RuntimeOptions,
|
||||||
) {
|
) {
|
||||||
return async (event) => {
|
return async (event) => {
|
||||||
const app = await createApp(config, args);
|
const app = await createApp(config, args, opts);
|
||||||
return await handle(app.server)(event);
|
return await handle(app.server)(event);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ describe("aws adapter", () => {
|
|||||||
adapterTestSuite(bunTestRunner, {
|
adapterTestSuite(bunTestRunner, {
|
||||||
makeApp: awsLambda.createApp,
|
makeApp: awsLambda.createApp,
|
||||||
// @todo: add a request to lambda event translator?
|
// @todo: add a request to lambda event translator?
|
||||||
makeHandler: (c, a) => async (request: Request) => {
|
makeHandler: (c, a, o) => async (request: Request) => {
|
||||||
const app = await awsLambda.createApp(c, a);
|
const app = await awsLambda.createApp(c, a, o);
|
||||||
return app.fetch(request);
|
return app.fetch(request);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/// <reference types="bun-types" />
|
/// <reference types="bun-types" />
|
||||||
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter";
|
import { type RuntimeBkndConfig, createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
|
||||||
import { registerLocalMediaAdapter } from ".";
|
import { registerLocalMediaAdapter } from ".";
|
||||||
import { config, type App } from "bknd";
|
import { config, type App } from "bknd";
|
||||||
import type { ServeOptions } from "bun";
|
import type { ServeOptions } from "bun";
|
||||||
@@ -11,33 +11,32 @@ type BunEnv = Bun.Env;
|
|||||||
export type BunBkndConfig<Env = BunEnv> = RuntimeBkndConfig<Env> & Omit<ServeOptions, "fetch">;
|
export type BunBkndConfig<Env = BunEnv> = RuntimeBkndConfig<Env> & Omit<ServeOptions, "fetch">;
|
||||||
|
|
||||||
export async function createApp<Env = BunEnv>(
|
export async function createApp<Env = BunEnv>(
|
||||||
{ distPath, serveStatic: _serveStatic, ...config }: BunBkndConfig<Env> = {},
|
{ distPath, ...config }: BunBkndConfig<Env> = {},
|
||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
|
opts?: RuntimeOptions,
|
||||||
) {
|
) {
|
||||||
const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static");
|
const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static");
|
||||||
registerLocalMediaAdapter();
|
registerLocalMediaAdapter();
|
||||||
|
|
||||||
return await createRuntimeApp(
|
return await createRuntimeApp(
|
||||||
{
|
{
|
||||||
serveStatic:
|
serveStatic: serveStatic({ root }),
|
||||||
_serveStatic ??
|
|
||||||
serveStatic({
|
|
||||||
root,
|
|
||||||
}),
|
|
||||||
...config,
|
...config,
|
||||||
},
|
},
|
||||||
args ?? (process.env as Env),
|
args ?? (process.env as Env),
|
||||||
|
opts,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createHandler<Env = BunEnv>(
|
export function createHandler<Env = BunEnv>(
|
||||||
config: BunBkndConfig<Env> = {},
|
config: BunBkndConfig<Env> = {},
|
||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
|
opts?: RuntimeOptions,
|
||||||
) {
|
) {
|
||||||
let app: App | undefined;
|
let app: App | undefined;
|
||||||
return async (req: Request) => {
|
return async (req: Request) => {
|
||||||
if (!app) {
|
if (!app) {
|
||||||
app = await createApp(config, args ?? (process.env as Env));
|
app = await createApp(config, args ?? (process.env as Env), opts);
|
||||||
}
|
}
|
||||||
return app.fetch(req);
|
return app.fetch(req);
|
||||||
};
|
};
|
||||||
@@ -47,7 +46,7 @@ export function serve<Env = BunEnv>(
|
|||||||
{
|
{
|
||||||
distPath,
|
distPath,
|
||||||
connection,
|
connection,
|
||||||
config: _config,
|
initialConfig,
|
||||||
options,
|
options,
|
||||||
port = config.server.default_port,
|
port = config.server.default_port,
|
||||||
onBuilt,
|
onBuilt,
|
||||||
@@ -57,6 +56,7 @@ export function serve<Env = BunEnv>(
|
|||||||
...serveOptions
|
...serveOptions
|
||||||
}: BunBkndConfig<Env> = {},
|
}: BunBkndConfig<Env> = {},
|
||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
|
opts?: RuntimeOptions,
|
||||||
) {
|
) {
|
||||||
Bun.serve({
|
Bun.serve({
|
||||||
...serveOptions,
|
...serveOptions,
|
||||||
@@ -64,7 +64,7 @@ export function serve<Env = BunEnv>(
|
|||||||
fetch: createHandler(
|
fetch: createHandler(
|
||||||
{
|
{
|
||||||
connection,
|
connection,
|
||||||
config: _config,
|
initialConfig,
|
||||||
options,
|
options,
|
||||||
onBuilt,
|
onBuilt,
|
||||||
buildConfig,
|
buildConfig,
|
||||||
@@ -73,6 +73,7 @@ export function serve<Env = BunEnv>(
|
|||||||
serveStatic,
|
serveStatic,
|
||||||
},
|
},
|
||||||
args,
|
args,
|
||||||
|
opts,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { connectionTestSuite } from "data/connection/connection-test-suite";
|
import { connectionTestSuite } from "data/connection/connection-test-suite";
|
||||||
import { bunSqlite } from "./BunSqliteConnection";
|
import { bunSqlite } from "./BunSqliteConnection";
|
||||||
import { bunTestRunner } from "adapter/bun/test";
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
import { describe, test, mock, expect } from "bun:test";
|
import { describe } from "bun:test";
|
||||||
import { Database } from "bun:sqlite";
|
import { Database } from "bun:sqlite";
|
||||||
import { GenericSqliteConnection } from "data/connection/sqlite/GenericSqliteConnection";
|
|
||||||
|
|
||||||
describe("BunSqliteConnection", () => {
|
describe("BunSqliteConnection", () => {
|
||||||
connectionTestSuite(bunTestRunner, {
|
connectionTestSuite(bunTestRunner, {
|
||||||
@@ -13,20 +12,4 @@ describe("BunSqliteConnection", () => {
|
|||||||
}),
|
}),
|
||||||
rawDialectDetails: [],
|
rawDialectDetails: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
test("onCreateConnection", async () => {
|
|
||||||
const called = mock(() => null);
|
|
||||||
|
|
||||||
const conn = bunSqlite({
|
|
||||||
onCreateConnection: (db) => {
|
|
||||||
expect(db).toBeInstanceOf(Database);
|
|
||||||
called();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await conn.ping();
|
|
||||||
|
|
||||||
expect(conn).toBeInstanceOf(GenericSqliteConnection);
|
|
||||||
expect(conn.db).toBeInstanceOf(Database);
|
|
||||||
expect(called).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,53 +1,40 @@
|
|||||||
import { Database } from "bun:sqlite";
|
import { Database } from "bun:sqlite";
|
||||||
import {
|
import { genericSqlite, type GenericSqliteConnection } from "bknd";
|
||||||
genericSqlite,
|
|
||||||
type GenericSqliteConnection,
|
|
||||||
type GenericSqliteConnectionConfig,
|
|
||||||
} from "bknd";
|
|
||||||
import { omitKeys } from "bknd/utils";
|
|
||||||
|
|
||||||
export type BunSqliteConnection = GenericSqliteConnection<Database>;
|
export type BunSqliteConnection = GenericSqliteConnection<Database>;
|
||||||
export type BunSqliteConnectionConfig = Omit<
|
export type BunSqliteConnectionConfig = {
|
||||||
GenericSqliteConnectionConfig<Database>,
|
database: Database;
|
||||||
"name" | "supports"
|
};
|
||||||
> &
|
|
||||||
({ database?: Database; url?: never } | { url?: string; database?: never });
|
|
||||||
|
|
||||||
export function bunSqlite(config?: BunSqliteConnectionConfig) {
|
export function bunSqlite(config?: BunSqliteConnectionConfig | { url: string }) {
|
||||||
let db: Database | undefined;
|
let db: Database;
|
||||||
if (config) {
|
if (config) {
|
||||||
if ("database" in config && config.database) {
|
if ("database" in config) {
|
||||||
db = config.database;
|
db = config.database;
|
||||||
} else if (config.url) {
|
} else {
|
||||||
db = new Database(config.url);
|
db = new Database(config.url);
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
|
||||||
if (!db) {
|
|
||||||
db = new Database(":memory:");
|
db = new Database(":memory:");
|
||||||
}
|
}
|
||||||
|
|
||||||
return genericSqlite(
|
return genericSqlite("bun-sqlite", db, (utils) => {
|
||||||
"bun-sqlite",
|
//const fn = cache ? "query" : "prepare";
|
||||||
db,
|
const getStmt = (sql: string) => db.prepare(sql);
|
||||||
(utils) => {
|
|
||||||
const getStmt = (sql: string) => db.prepare(sql);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
db,
|
db,
|
||||||
query: utils.buildQueryFn({
|
query: utils.buildQueryFn({
|
||||||
all: (sql, parameters) => getStmt(sql).all(...(parameters || [])),
|
all: (sql, parameters) => getStmt(sql).all(...(parameters || [])),
|
||||||
run: (sql, parameters) => {
|
run: (sql, parameters) => {
|
||||||
const { changes, lastInsertRowid } = getStmt(sql).run(...(parameters || []));
|
const { changes, lastInsertRowid } = getStmt(sql).run(...(parameters || []));
|
||||||
return {
|
return {
|
||||||
insertId: utils.parseBigInt(lastInsertRowid),
|
insertId: utils.parseBigInt(lastInsertRowid),
|
||||||
numAffectedRows: utils.parseBigInt(changes),
|
numAffectedRows: utils.parseBigInt(changes),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
close: () => db.close(),
|
close: () => db.close(),
|
||||||
};
|
};
|
||||||
},
|
});
|
||||||
omitKeys(config ?? ({} as any), ["database", "url", "name", "supports"]),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { expect, test, mock, describe, beforeEach, afterEach, afterAll, beforeAll } from "bun:test";
|
import { expect, test, mock, describe, beforeEach, afterEach, afterAll } from "bun:test";
|
||||||
|
|
||||||
export const bunTestRunner = {
|
export const bunTestRunner = {
|
||||||
describe,
|
describe,
|
||||||
@@ -8,5 +8,4 @@ export const bunTestRunner = {
|
|||||||
beforeEach,
|
beforeEach,
|
||||||
afterEach,
|
afterEach,
|
||||||
afterAll,
|
afterAll,
|
||||||
beforeAll,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { inspect } from "node:util";
|
|
||||||
|
|
||||||
export type BindingTypeMap = {
|
export type BindingTypeMap = {
|
||||||
D1Database: D1Database;
|
D1Database: D1Database;
|
||||||
KVNamespace: KVNamespace;
|
KVNamespace: KVNamespace;
|
||||||
@@ -15,9 +13,8 @@ export function getBindings<T extends GetBindingType>(env: any, type: T): Bindin
|
|||||||
for (const key in env) {
|
for (const key in env) {
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
(env[key] as any).constructor.name === type ||
|
env[key] &&
|
||||||
String(env[key]) === `[object ${type}]` ||
|
((env[key] as any).constructor.name === type || String(env[key]) === `[object ${type}]`)
|
||||||
inspect(env[key]).includes(type)
|
|
||||||
) {
|
) {
|
||||||
bindings.push({
|
bindings.push({
|
||||||
key,
|
key,
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||||
import { makeConfig, type CloudflareContext } from "./config";
|
import { makeApp } from "./modes/fresh";
|
||||||
|
import { makeConfig, type CfMakeConfigArgs } from "./config";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||||
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||||
import { bunTestRunner } from "adapter/bun/test";
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
import { type CloudflareBkndConfig, createApp } from "./cloudflare-workers.adapter";
|
import type { CloudflareBkndConfig } from "./cloudflare-workers.adapter";
|
||||||
|
|
||||||
/* beforeAll(disableConsoleLog);
|
beforeAll(disableConsoleLog);
|
||||||
afterAll(enableConsoleLog); */
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
describe("cf adapter", () => {
|
describe("cf adapter", () => {
|
||||||
const DB_URL = ":memory:";
|
const DB_URL = ":memory:";
|
||||||
@@ -17,42 +18,42 @@ describe("cf adapter", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("makes config", async () => {
|
it("makes config", async () => {
|
||||||
const staticConfig = await makeConfig(
|
const staticConfig = makeConfig(
|
||||||
{
|
{
|
||||||
connection: { url: DB_URL },
|
connection: { url: DB_URL },
|
||||||
config: { data: { basepath: DB_URL } },
|
initialConfig: { data: { basepath: DB_URL } },
|
||||||
},
|
},
|
||||||
$ctx({ DB_URL }),
|
$ctx({ DB_URL }),
|
||||||
);
|
);
|
||||||
expect(staticConfig.config).toEqual({ data: { basepath: DB_URL } });
|
expect(staticConfig.initialConfig).toEqual({ data: { basepath: DB_URL } });
|
||||||
expect(staticConfig.connection).toBeDefined();
|
expect(staticConfig.connection).toBeDefined();
|
||||||
|
|
||||||
const dynamicConfig = await makeConfig(
|
const dynamicConfig = makeConfig(
|
||||||
{
|
{
|
||||||
app: (env) => ({
|
app: (env) => ({
|
||||||
config: { data: { basepath: env.DB_URL } },
|
initialConfig: { data: { basepath: env.DB_URL } },
|
||||||
connection: { url: env.DB_URL },
|
connection: { url: env.DB_URL },
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
$ctx({ DB_URL }),
|
$ctx({ DB_URL }),
|
||||||
);
|
);
|
||||||
expect(dynamicConfig.config).toEqual({ data: { basepath: DB_URL } });
|
expect(dynamicConfig.initialConfig).toEqual({ data: { basepath: DB_URL } });
|
||||||
expect(dynamicConfig.connection).toBeDefined();
|
expect(dynamicConfig.connection).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
adapterTestSuite<CloudflareBkndConfig, CloudflareContext<any>>(bunTestRunner, {
|
adapterTestSuite<CloudflareBkndConfig, CfMakeConfigArgs<any>>(bunTestRunner, {
|
||||||
makeApp: async (c, a) => {
|
makeApp: async (c, a, o) => {
|
||||||
return await createApp(c, { env: a } as any);
|
return await makeApp(c, { env: a } as any, o);
|
||||||
},
|
},
|
||||||
makeHandler: (c, a) => {
|
makeHandler: (c, a, o) => {
|
||||||
console.log("args", a);
|
|
||||||
return async (request: any) => {
|
return async (request: any) => {
|
||||||
const app = await createApp(
|
const app = await makeApp(
|
||||||
// needs a fallback, otherwise tries to launch D1
|
// needs a fallback, otherwise tries to launch D1
|
||||||
c ?? {
|
c ?? {
|
||||||
connection: { url: DB_URL },
|
connection: { url: DB_URL },
|
||||||
},
|
},
|
||||||
a as any,
|
a!,
|
||||||
|
o,
|
||||||
);
|
);
|
||||||
return app.fetch(request);
|
return app.fetch(request);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
import type { RuntimeBkndConfig } from "bknd/adapter";
|
import type { RuntimeBkndConfig } from "bknd/adapter";
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { serveStatic } from "hono/cloudflare-workers";
|
import { serveStatic } from "hono/cloudflare-workers";
|
||||||
import type { MaybePromise } from "bknd";
|
import { getFresh } from "./modes/fresh";
|
||||||
import { $console } from "bknd/utils";
|
import { getCached } from "./modes/cached";
|
||||||
import { createRuntimeApp } from "bknd/adapter";
|
import { getDurable } from "./modes/durable";
|
||||||
import { registerAsyncsExecutionContext, makeConfig, type CloudflareContext } from "./config";
|
import type { App } from "bknd";
|
||||||
|
import { $console } from "core/utils";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace Cloudflare {
|
namespace Cloudflare {
|
||||||
@@ -16,10 +17,12 @@ declare global {
|
|||||||
|
|
||||||
export type CloudflareEnv = Cloudflare.Env;
|
export type CloudflareEnv = Cloudflare.Env;
|
||||||
export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> & {
|
export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> & {
|
||||||
bindings?: (args: Env) => MaybePromise<{
|
mode?: "warm" | "fresh" | "cache" | "durable";
|
||||||
|
bindings?: (args: Env) => {
|
||||||
kv?: KVNamespace;
|
kv?: KVNamespace;
|
||||||
|
dobj?: DurableObjectNamespace;
|
||||||
db?: D1Database;
|
db?: D1Database;
|
||||||
}>;
|
};
|
||||||
d1?: {
|
d1?: {
|
||||||
session?: boolean;
|
session?: boolean;
|
||||||
transport?: "header" | "cookie";
|
transport?: "header" | "cookie";
|
||||||
@@ -33,27 +36,11 @@ export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> &
|
|||||||
registerMedia?: boolean | ((env: Env) => void);
|
registerMedia?: boolean | ((env: Env) => void);
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function createApp<Env extends CloudflareEnv = CloudflareEnv>(
|
export type Context<Env = CloudflareEnv> = {
|
||||||
config: CloudflareBkndConfig<Env> = {},
|
request: Request;
|
||||||
ctx: Partial<CloudflareContext<Env>> = {},
|
env: Env;
|
||||||
) {
|
ctx: ExecutionContext;
|
||||||
const appConfig = await makeConfig(
|
};
|
||||||
{
|
|
||||||
...config,
|
|
||||||
onBuilt: async (app) => {
|
|
||||||
if (ctx.ctx) {
|
|
||||||
registerAsyncsExecutionContext(app, ctx?.ctx);
|
|
||||||
}
|
|
||||||
await config.onBuilt?.(app);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ctx,
|
|
||||||
);
|
|
||||||
return await createRuntimeApp<Env>(appConfig, ctx?.env);
|
|
||||||
}
|
|
||||||
|
|
||||||
// compatiblity
|
|
||||||
export const getFresh = createApp;
|
|
||||||
|
|
||||||
export function serve<Env extends CloudflareEnv = CloudflareEnv>(
|
export function serve<Env extends CloudflareEnv = CloudflareEnv>(
|
||||||
config: CloudflareBkndConfig<Env> = {},
|
config: CloudflareBkndConfig<Env> = {},
|
||||||
@@ -92,8 +79,25 @@ export function serve<Env extends CloudflareEnv = CloudflareEnv>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const context = { request, env, ctx } as CloudflareContext<Env>;
|
const context = { request, env, ctx } as Context<Env>;
|
||||||
const app = await createApp(config, context);
|
const mode = config.mode ?? "warm";
|
||||||
|
|
||||||
|
let app: App;
|
||||||
|
switch (mode) {
|
||||||
|
case "fresh":
|
||||||
|
app = await getFresh(config, context, { force: true });
|
||||||
|
break;
|
||||||
|
case "warm":
|
||||||
|
app = await getFresh(config, context);
|
||||||
|
break;
|
||||||
|
case "cache":
|
||||||
|
app = await getCached(config, context);
|
||||||
|
break;
|
||||||
|
case "durable":
|
||||||
|
return await getDurable(config, context);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown mode ${mode}`);
|
||||||
|
}
|
||||||
|
|
||||||
return app.fetch(request, env, ctx);
|
return app.fetch(request, env, ctx);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import { getBinding } from "./bindings";
|
|||||||
import { d1Sqlite } 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 type { Context as HonoContext, ExecutionContext } from "hono";
|
import type { Context, ExecutionContext } from "hono";
|
||||||
import { $console } from "bknd/utils";
|
import { $console } from "core/utils";
|
||||||
import { setCookie } from "hono/cookie";
|
import { setCookie } from "hono/cookie";
|
||||||
|
|
||||||
export const constants = {
|
export const constants = {
|
||||||
@@ -22,10 +22,10 @@ export const constants = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CloudflareContext<Env extends CloudflareEnv = CloudflareEnv> = {
|
export type CfMakeConfigArgs<Env extends CloudflareEnv = CloudflareEnv> = {
|
||||||
env: Env;
|
env: Env;
|
||||||
ctx: ExecutionContext;
|
ctx?: ExecutionContext;
|
||||||
request: Request;
|
request?: Request;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getCookieValue(cookies: string | null, name: string) {
|
function getCookieValue(cookies: string | null, name: string) {
|
||||||
@@ -67,7 +67,7 @@ export function d1SessionHelper(config: CloudflareBkndConfig<any>) {
|
|||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
},
|
},
|
||||||
set: (c: HonoContext, d1?: D1DatabaseSession) => {
|
set: (c: Context, d1?: D1DatabaseSession) => {
|
||||||
if (!d1 || !config.d1?.session) return;
|
if (!d1 || !config.d1?.session) return;
|
||||||
|
|
||||||
const session = d1.getBookmark();
|
const session = d1.getBookmark();
|
||||||
@@ -89,9 +89,9 @@ export function d1SessionHelper(config: CloudflareBkndConfig<any>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let media_registered: boolean = false;
|
let media_registered: boolean = false;
|
||||||
export async function makeConfig<Env extends CloudflareEnv = CloudflareEnv>(
|
export function makeConfig<Env extends CloudflareEnv = CloudflareEnv>(
|
||||||
config: CloudflareBkndConfig<Env>,
|
config: CloudflareBkndConfig<Env>,
|
||||||
args?: Partial<CloudflareContext<Env>>,
|
args?: CfMakeConfigArgs<Env>,
|
||||||
) {
|
) {
|
||||||
if (!media_registered && config.registerMedia !== false) {
|
if (!media_registered && config.registerMedia !== false) {
|
||||||
if (typeof config.registerMedia === "function") {
|
if (typeof config.registerMedia === "function") {
|
||||||
@@ -102,7 +102,7 @@ export async function makeConfig<Env extends CloudflareEnv = CloudflareEnv>(
|
|||||||
media_registered = true;
|
media_registered = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const appConfig = await makeAdapterConfig(config, args?.env);
|
const appConfig = makeAdapterConfig(config, args?.env);
|
||||||
|
|
||||||
// if connection instance is given, don't do anything
|
// if connection instance is given, don't do anything
|
||||||
// other than checking if D1 session is defined
|
// other than checking if D1 session is defined
|
||||||
@@ -115,12 +115,12 @@ export async function makeConfig<Env extends CloudflareEnv = CloudflareEnv>(
|
|||||||
}
|
}
|
||||||
// if connection is given, try to open with unified sqlite adapter
|
// if connection is given, try to open with unified sqlite adapter
|
||||||
} else if (appConfig.connection) {
|
} else if (appConfig.connection) {
|
||||||
appConfig.connection = sqlite(appConfig.connection) as any;
|
appConfig.connection = sqlite(appConfig.connection);
|
||||||
|
|
||||||
// if connection is not given, but env is set
|
// if connection is not given, but env is set
|
||||||
// try to make D1 from bindings
|
// try to make D1 from bindings
|
||||||
} else if (args?.env) {
|
} else if (args?.env) {
|
||||||
const bindings = await config.bindings?.(args?.env);
|
const bindings = config.bindings?.(args?.env);
|
||||||
const sessionHelper = d1SessionHelper(config);
|
const sessionHelper = d1SessionHelper(config);
|
||||||
const sessionId = sessionHelper.get(args.request);
|
const sessionId = sessionHelper.get(args.request);
|
||||||
let session: D1DatabaseSession | undefined;
|
let session: D1DatabaseSession | undefined;
|
||||||
|
|||||||
@@ -3,16 +3,16 @@
|
|||||||
import { genericSqlite, type GenericSqliteConnection } from "bknd";
|
import { genericSqlite, type GenericSqliteConnection } from "bknd";
|
||||||
import type { QueryResult } from "kysely";
|
import type { QueryResult } from "kysely";
|
||||||
|
|
||||||
export type DoSqliteConnection = GenericSqliteConnection<DurableObjectState["storage"]["sql"]>;
|
export type D1SqliteConnection = GenericSqliteConnection<D1Database>;
|
||||||
export type DurableObjecSql = DurableObjectState["storage"]["sql"];
|
export type DurableObjecSql = DurableObjectState["storage"]["sql"];
|
||||||
|
|
||||||
export type DoConnectionConfig<DB extends DurableObjecSql> =
|
export type D1ConnectionConfig<DB extends DurableObjecSql> =
|
||||||
| DurableObjectState
|
| DurableObjectState
|
||||||
| {
|
| {
|
||||||
sql: DB;
|
sql: DB;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function doSqlite<DB extends DurableObjecSql>(config: DoConnectionConfig<DB>) {
|
export function doSqlite<DB extends DurableObjecSql>(config: D1ConnectionConfig<DB>) {
|
||||||
const db = "sql" in config ? config.sql : config.storage.sql;
|
const db = "sql" in config ? config.sql : config.storage.sql;
|
||||||
|
|
||||||
return genericSqlite(
|
return genericSqlite(
|
||||||
@@ -21,7 +21,7 @@ export function doSqlite<DB extends DurableObjecSql>(config: DoConnectionConfig<
|
|||||||
(utils) => {
|
(utils) => {
|
||||||
// must be async to work with the miniflare mock
|
// must be async to work with the miniflare mock
|
||||||
const getStmt = async (sql: string, parameters?: any[] | readonly any[]) =>
|
const getStmt = async (sql: string, parameters?: any[] | readonly any[]) =>
|
||||||
db.exec(sql, ...(parameters || []));
|
await db.exec(sql, ...(parameters || []));
|
||||||
|
|
||||||
const mapResult = (
|
const mapResult = (
|
||||||
cursor: SqlStorageCursor<Record<string, SqlStorageValue>>,
|
cursor: SqlStorageCursor<Record<string, SqlStorageValue>>,
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
/// <reference types="@cloudflare/workers-types" />
|
/// <reference types="@cloudflare/workers-types" />
|
||||||
|
|
||||||
import { describe, beforeAll, afterAll } from "vitest";
|
import { describe, test, expect } from "vitest";
|
||||||
|
|
||||||
import { viTestRunner } from "adapter/node/vitest";
|
import { viTestRunner } from "adapter/node/vitest";
|
||||||
import { connectionTestSuite } from "data/connection/connection-test-suite";
|
import { connectionTestSuite } from "data/connection/connection-test-suite";
|
||||||
import { Miniflare } from "miniflare";
|
import { Miniflare } from "miniflare";
|
||||||
import { doSqlite } from "./DoConnection";
|
import { doSqlite } from "./DoConnection";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
const script = `
|
const script = `
|
||||||
import { DurableObject } from "cloudflare:workers";
|
import { DurableObject } from "cloudflare:workers";
|
||||||
@@ -41,9 +40,6 @@ export default {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
afterAll(() => enableConsoleLog());
|
|
||||||
|
|
||||||
describe("doSqlite", async () => {
|
describe("doSqlite", async () => {
|
||||||
connectionTestSuite(viTestRunner, {
|
connectionTestSuite(viTestRunner, {
|
||||||
makeConnection: async () => {
|
makeConnection: async () => {
|
||||||
|
|||||||
@@ -3,10 +3,6 @@ import { cacheWorkersKV } from "./cache";
|
|||||||
import { viTestRunner } from "adapter/node/vitest";
|
import { viTestRunner } from "adapter/node/vitest";
|
||||||
import { cacheDriverTestSuite } from "core/drivers/cache/cache-driver-test-suite";
|
import { cacheDriverTestSuite } from "core/drivers/cache/cache-driver-test-suite";
|
||||||
import { Miniflare } from "miniflare";
|
import { Miniflare } from "miniflare";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
afterAll(() => enableConsoleLog());
|
|
||||||
|
|
||||||
describe("cacheWorkersKV", async () => {
|
describe("cacheWorkersKV", async () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import { d1Sqlite, type D1ConnectionConfig } from "./connection/D1Connection";
|
import { d1Sqlite, type D1ConnectionConfig } from "./connection/D1Connection";
|
||||||
|
|
||||||
export {
|
export * from "./cloudflare-workers.adapter";
|
||||||
getFresh,
|
export { makeApp, getFresh } from "./modes/fresh";
|
||||||
createApp,
|
export { getCached } from "./modes/cached";
|
||||||
serve,
|
export { DurableBkndApp, getDurable } from "./modes/durable";
|
||||||
type CloudflareEnv,
|
|
||||||
type CloudflareBkndConfig,
|
|
||||||
} from "./cloudflare-workers.adapter";
|
|
||||||
export { d1Sqlite, type D1ConnectionConfig };
|
export { d1Sqlite, type D1ConnectionConfig };
|
||||||
export { doSqlite, type DoConnectionConfig } from "./connection/DoConnection";
|
|
||||||
export {
|
export {
|
||||||
getBinding,
|
getBinding,
|
||||||
getBindings,
|
getBindings,
|
||||||
@@ -16,10 +12,9 @@ export {
|
|||||||
type GetBindingType,
|
type GetBindingType,
|
||||||
type BindingMap,
|
type BindingMap,
|
||||||
} from "./bindings";
|
} from "./bindings";
|
||||||
export { constants, makeConfig, type CloudflareContext } from "./config";
|
export { constants } from "./config";
|
||||||
export { StorageR2Adapter, registerMedia } from "./storage/StorageR2Adapter";
|
export { StorageR2Adapter, registerMedia } from "./storage/StorageR2Adapter";
|
||||||
export { registries } from "bknd";
|
export { registries } from "bknd";
|
||||||
export { devFsVitePlugin, devFsWrite } from "./vite";
|
|
||||||
|
|
||||||
// for compatibility with old code
|
// for compatibility with old code
|
||||||
export function d1<DB extends D1Database | D1DatabaseSession = D1Database>(
|
export function d1<DB extends D1Database | D1DatabaseSession = D1Database>(
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { App } from "bknd";
|
||||||
|
import { createRuntimeApp } from "bknd/adapter";
|
||||||
|
import type { CloudflareBkndConfig, Context, CloudflareEnv } from "../index";
|
||||||
|
import { makeConfig, registerAsyncsExecutionContext, constants } from "../config";
|
||||||
|
|
||||||
|
export async function getCached<Env extends CloudflareEnv = CloudflareEnv>(
|
||||||
|
config: CloudflareBkndConfig<Env>,
|
||||||
|
args: Context<Env>,
|
||||||
|
) {
|
||||||
|
const { env, ctx } = args;
|
||||||
|
const { kv } = config.bindings?.(env)!;
|
||||||
|
if (!kv) throw new Error("kv namespace is not defined in cloudflare.bindings");
|
||||||
|
const key = config.key ?? "app";
|
||||||
|
|
||||||
|
const cachedConfig = await kv.get(key);
|
||||||
|
const initialConfig = cachedConfig ? JSON.parse(cachedConfig) : undefined;
|
||||||
|
|
||||||
|
async function saveConfig(__config: any) {
|
||||||
|
ctx.waitUntil(kv!.put(key, JSON.stringify(__config)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = await createRuntimeApp(
|
||||||
|
{
|
||||||
|
...makeConfig(config, args),
|
||||||
|
initialConfig,
|
||||||
|
onBuilt: async (app) => {
|
||||||
|
registerAsyncsExecutionContext(app, ctx);
|
||||||
|
app.module.server.client.get(constants.cache_endpoint, async (c) => {
|
||||||
|
await kv.delete(key);
|
||||||
|
return c.json({ message: "Cache cleared" });
|
||||||
|
});
|
||||||
|
await config.onBuilt?.(app);
|
||||||
|
},
|
||||||
|
beforeBuild: async (app) => {
|
||||||
|
app.emgr.onEvent(
|
||||||
|
App.Events.AppConfigUpdatedEvent,
|
||||||
|
async ({ params: { app } }) => {
|
||||||
|
saveConfig(app.toJSON(true));
|
||||||
|
},
|
||||||
|
"sync",
|
||||||
|
);
|
||||||
|
await config.beforeBuild?.(app);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!cachedConfig) {
|
||||||
|
saveConfig(app.toJSON(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { DurableObject } from "cloudflare:workers";
|
||||||
|
import type { App, CreateAppConfig } from "bknd";
|
||||||
|
import { createRuntimeApp, makeConfig } from "bknd/adapter";
|
||||||
|
import type { CloudflareBkndConfig, Context, CloudflareEnv } from "../index";
|
||||||
|
import { constants, registerAsyncsExecutionContext } from "../config";
|
||||||
|
import { $console } from "core/utils";
|
||||||
|
|
||||||
|
export async function getDurable<Env extends CloudflareEnv = CloudflareEnv>(
|
||||||
|
config: CloudflareBkndConfig<Env>,
|
||||||
|
ctx: Context<Env>,
|
||||||
|
) {
|
||||||
|
const { dobj } = config.bindings?.(ctx.env)!;
|
||||||
|
if (!dobj) throw new Error("durable object is not defined in cloudflare.bindings");
|
||||||
|
const key = config.key ?? "app";
|
||||||
|
|
||||||
|
if ([config.onBuilt, config.beforeBuild].some((x) => x)) {
|
||||||
|
$console.warn("onBuilt and beforeBuild are not supported with DurableObject mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = performance.now();
|
||||||
|
|
||||||
|
const id = dobj.idFromName(key);
|
||||||
|
const stub = dobj.get(id) as unknown as DurableBkndApp;
|
||||||
|
|
||||||
|
const create_config = makeConfig(config, ctx.env);
|
||||||
|
|
||||||
|
const res = await stub.fire(ctx.request, {
|
||||||
|
config: create_config,
|
||||||
|
keepAliveSeconds: config.keepAliveSeconds,
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers = new Headers(res.headers);
|
||||||
|
headers.set("X-TTDO", String(performance.now() - start));
|
||||||
|
|
||||||
|
return new Response(res.body, {
|
||||||
|
status: res.status,
|
||||||
|
statusText: res.statusText,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DurableBkndApp extends DurableObject {
|
||||||
|
protected id = Math.random().toString(36).slice(2);
|
||||||
|
protected app?: App;
|
||||||
|
protected interval?: any;
|
||||||
|
|
||||||
|
async fire(
|
||||||
|
request: Request,
|
||||||
|
options: {
|
||||||
|
config: CreateAppConfig;
|
||||||
|
html?: string;
|
||||||
|
keepAliveSeconds?: number;
|
||||||
|
setAdminHtml?: boolean;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
let buildtime = 0;
|
||||||
|
if (!this.app) {
|
||||||
|
const start = performance.now();
|
||||||
|
const config = options.config;
|
||||||
|
|
||||||
|
// change protocol to websocket if libsql
|
||||||
|
if (
|
||||||
|
config?.connection &&
|
||||||
|
"type" in config.connection &&
|
||||||
|
config.connection.type === "libsql"
|
||||||
|
) {
|
||||||
|
//config.connection.config.protocol = "wss";
|
||||||
|
}
|
||||||
|
|
||||||
|
this.app = await createRuntimeApp({
|
||||||
|
...config,
|
||||||
|
onBuilt: async (app) => {
|
||||||
|
registerAsyncsExecutionContext(app, this.ctx);
|
||||||
|
app.modules.server.get(constants.do_endpoint, async (c) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const context: any = c.req.raw.cf ? c.req.raw.cf : c.env.cf;
|
||||||
|
return c.json({
|
||||||
|
id: this.id,
|
||||||
|
keepAliveSeconds: options?.keepAliveSeconds ?? 0,
|
||||||
|
colo: context.colo,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.onBuilt(app);
|
||||||
|
},
|
||||||
|
adminOptions: { html: options.html },
|
||||||
|
beforeBuild: async (app) => {
|
||||||
|
await this.beforeBuild(app);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
buildtime = performance.now() - start;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.keepAliveSeconds) {
|
||||||
|
this.keepAlive(options.keepAliveSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await this.app!.fetch(request);
|
||||||
|
const headers = new Headers(res.headers);
|
||||||
|
headers.set("X-BuildTime", buildtime.toString());
|
||||||
|
headers.set("X-DO-ID", this.id);
|
||||||
|
|
||||||
|
return new Response(res.body, {
|
||||||
|
status: res.status,
|
||||||
|
statusText: res.statusText,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onBuilt(app: App) {}
|
||||||
|
|
||||||
|
async beforeBuild(app: App) {}
|
||||||
|
|
||||||
|
protected keepAlive(seconds: number) {
|
||||||
|
if (this.interval) {
|
||||||
|
clearInterval(this.interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
this.interval = setInterval(() => {
|
||||||
|
i += 1;
|
||||||
|
if (i === seconds) {
|
||||||
|
console.log("cleared");
|
||||||
|
clearInterval(this.interval);
|
||||||
|
|
||||||
|
// ping every 30 seconds
|
||||||
|
} else if (i % 30 === 0) {
|
||||||
|
console.log("ping");
|
||||||
|
this.app?.modules.ctx().connection.ping();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
|
||||||
|
import type { CloudflareBkndConfig, Context, CloudflareEnv } from "../index";
|
||||||
|
import { makeConfig, registerAsyncsExecutionContext, type CfMakeConfigArgs } from "../config";
|
||||||
|
|
||||||
|
export async function makeApp<Env extends CloudflareEnv = CloudflareEnv>(
|
||||||
|
config: CloudflareBkndConfig<Env>,
|
||||||
|
args?: CfMakeConfigArgs<Env>,
|
||||||
|
opts?: RuntimeOptions,
|
||||||
|
) {
|
||||||
|
return await createRuntimeApp<Env>(makeConfig(config, args), args?.env, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFresh<Env extends CloudflareEnv = CloudflareEnv>(
|
||||||
|
config: CloudflareBkndConfig<Env>,
|
||||||
|
ctx: Context<Env>,
|
||||||
|
opts: RuntimeOptions = {},
|
||||||
|
) {
|
||||||
|
return await makeApp(
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
onBuilt: async (app) => {
|
||||||
|
registerAsyncsExecutionContext(app, ctx.ctx);
|
||||||
|
await config.onBuilt?.(app);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ctx,
|
||||||
|
opts,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
import {
|
|
||||||
d1Sqlite,
|
|
||||||
getBinding,
|
|
||||||
registerMedia,
|
|
||||||
type CloudflareBkndConfig,
|
|
||||||
type CloudflareEnv,
|
|
||||||
} from "bknd/adapter/cloudflare";
|
|
||||||
import type { GetPlatformProxyOptions, PlatformProxy } from "wrangler";
|
|
||||||
import process from "node:process";
|
|
||||||
import { $console } from "bknd/utils";
|
|
||||||
|
|
||||||
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;
|
|
||||||
proxyOptions?: GetPlatformProxyOptions;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function getPlatformProxy(opts?: GetPlatformProxyOptions) {
|
|
||||||
try {
|
|
||||||
const { version } = await import("wrangler/package.json", { with: { type: "json" } }).then(
|
|
||||||
(pkg) => pkg.default,
|
|
||||||
);
|
|
||||||
$console.log("Using wrangler version", version);
|
|
||||||
const { getPlatformProxy } = await import("wrangler");
|
|
||||||
return getPlatformProxy(opts);
|
|
||||||
} catch (e) {
|
|
||||||
$console.error("Failed to import wrangler", String(e));
|
|
||||||
const resolved = import.meta.resolve("wrangler");
|
|
||||||
$console.log("Wrangler resolved to", resolved);
|
|
||||||
const file = resolved?.split("/").pop();
|
|
||||||
if (file?.endsWith(".json")) {
|
|
||||||
$console.error(
|
|
||||||
"You have a `wrangler.json` in your current directory. Please change to .jsonc or .toml",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
$console.log("Using cloudflare platform proxy");
|
|
||||||
|
|
||||||
async function getEnv(env?: Env): Promise<Env> {
|
|
||||||
if (use_proxy) {
|
|
||||||
if (!proxy) {
|
|
||||||
proxy = await getPlatformProxy(opts?.proxyOptions);
|
|
||||||
process.on("exit", () => {
|
|
||||||
proxy?.dispose();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
const binding = use_proxy ? getBinding(env, "D1Database") : undefined;
|
|
||||||
|
|
||||||
if (config?.app === undefined && use_proxy && binding) {
|
|
||||||
return {
|
|
||||||
connection: d1Sqlite({
|
|
||||||
binding: binding.value,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
} else if (typeof config?.app === "function") {
|
|
||||||
const appConfig = await config?.app(env);
|
|
||||||
if (binding) {
|
|
||||||
appConfig.connection = d1Sqlite({
|
|
||||||
binding: binding.value,
|
|
||||||
}) as any;
|
|
||||||
}
|
|
||||||
return appConfig;
|
|
||||||
}
|
|
||||||
return config?.app || {};
|
|
||||||
},
|
|
||||||
} satisfies CloudflareBkndConfig<Env>;
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { registries as $registries, isDebug, guessMimeType } from "bknd";
|
import { registries, isDebug, guessMimeType } from "bknd";
|
||||||
import { getBindings } from "../bindings";
|
import { getBindings } from "../bindings";
|
||||||
import { s } from "bknd/utils";
|
import { s } from "bknd/utils";
|
||||||
import { StorageAdapter, type FileBody } from "bknd";
|
import { StorageAdapter, type FileBody } from "bknd";
|
||||||
@@ -12,10 +12,7 @@ export function makeSchema(bindings: string[] = []) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerMedia(
|
export function registerMedia(env: Record<string, any>) {
|
||||||
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(
|
||||||
|
|||||||
@@ -3,12 +3,8 @@ import { Miniflare } from "miniflare";
|
|||||||
import { StorageR2Adapter } from "./StorageR2Adapter";
|
import { StorageR2Adapter } from "./StorageR2Adapter";
|
||||||
import { adapterTestSuite } from "media/storage/adapters/adapter-test-suite";
|
import { adapterTestSuite } from "media/storage/adapters/adapter-test-suite";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { describe, afterAll, test, expect, beforeAll } from "vitest";
|
import { describe, afterAll, test, expect } from "vitest";
|
||||||
import { viTestRunner } from "adapter/node/vitest";
|
import { viTestRunner } from "adapter/node/vitest";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
afterAll(() => enableConsoleLog());
|
|
||||||
|
|
||||||
let mf: Miniflare | undefined;
|
let mf: Miniflare | undefined;
|
||||||
describe("StorageR2Adapter", async () => {
|
describe("StorageR2Adapter", async () => {
|
||||||
@@ -28,8 +24,7 @@ describe("StorageR2Adapter", async () => {
|
|||||||
const buffer = readFileSync(path.join(basePath, "image.png"));
|
const buffer = readFileSync(path.join(basePath, "image.png"));
|
||||||
const file = new File([buffer], "image.png", { type: "image/png" });
|
const file = new File([buffer], "image.png", { type: "image/png" });
|
||||||
|
|
||||||
// miniflare doesn't support range requests
|
await adapterTestSuite(viTestRunner, adapter, file);
|
||||||
await adapterTestSuite(viTestRunner, adapter, file, { testRange: false });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
|||||||
@@ -1,272 +0,0 @@
|
|||||||
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;
|
|
||||||
} = {}): any {
|
|
||||||
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) {
|
|
||||||
verbose && console.debug("[dev-fs-plugin] Not in dev mode, skipping");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track active chunked requests
|
|
||||||
const activeRequests = new Map<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
totalChunks: number;
|
|
||||||
filename: string;
|
|
||||||
chunks: string[];
|
|
||||||
receivedChunks: number;
|
|
||||||
}
|
|
||||||
>();
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
|
|
||||||
// Skip our own debug output
|
|
||||||
if (output.includes("[dev-fs-plugin]") || output.includes("[dev-fs-polyfill]")) {
|
|
||||||
// @ts-ignore
|
|
||||||
// biome-ignore lint/style/noArguments: <explanation>
|
|
||||||
return originalStdoutWrite.apply(process.stdout, arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track if we process any protocol messages (to suppress output)
|
|
||||||
let processedProtocolMessage = false;
|
|
||||||
|
|
||||||
// Process all start markers in this output
|
|
||||||
if (output.includes("{{DEV_FS_START}}")) {
|
|
||||||
const startMatches = [
|
|
||||||
...output.matchAll(/{{DEV_FS_START}} ([a-z0-9]+) (\d+) (.+)/g),
|
|
||||||
];
|
|
||||||
for (const startMatch of startMatches) {
|
|
||||||
const requestId = startMatch[1];
|
|
||||||
const totalChunks = Number.parseInt(startMatch[2]);
|
|
||||||
const filename = startMatch[3];
|
|
||||||
|
|
||||||
activeRequests.set(requestId, {
|
|
||||||
totalChunks,
|
|
||||||
filename,
|
|
||||||
chunks: new Array(totalChunks),
|
|
||||||
receivedChunks: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
verbose &&
|
|
||||||
console.debug(
|
|
||||||
`[dev-fs-plugin] Started request ${requestId} for ${filename} (${totalChunks} chunks)`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
processedProtocolMessage = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process all chunk data in this output
|
|
||||||
if (output.includes("{{DEV_FS_CHUNK}}")) {
|
|
||||||
const chunkMatches = [
|
|
||||||
...output.matchAll(/{{DEV_FS_CHUNK}} ([a-z0-9]+) (\d+) ([A-Za-z0-9+/=]+)/g),
|
|
||||||
];
|
|
||||||
for (const chunkMatch of chunkMatches) {
|
|
||||||
const requestId = chunkMatch[1];
|
|
||||||
const chunkIndex = Number.parseInt(chunkMatch[2]);
|
|
||||||
const chunkData = chunkMatch[3];
|
|
||||||
|
|
||||||
const request = activeRequests.get(requestId);
|
|
||||||
if (request) {
|
|
||||||
request.chunks[chunkIndex] = chunkData;
|
|
||||||
request.receivedChunks++;
|
|
||||||
verbose &&
|
|
||||||
console.debug(
|
|
||||||
`[dev-fs-plugin] Received chunk ${chunkIndex}/${request.totalChunks - 1} for ${request.filename} (length: ${chunkData.length})`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Validate base64 chunk
|
|
||||||
if (chunkData.length < 1000 && chunkIndex < request.totalChunks - 1) {
|
|
||||||
verbose &&
|
|
||||||
console.warn(
|
|
||||||
`[dev-fs-plugin] WARNING: Chunk ${chunkIndex} seems truncated (length: ${chunkData.length})`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
processedProtocolMessage = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process all end markers in this output
|
|
||||||
if (output.includes("{{DEV_FS_END}}")) {
|
|
||||||
const endMatches = [...output.matchAll(/{{DEV_FS_END}} ([a-z0-9]+)/g)];
|
|
||||||
for (const endMatch of endMatches) {
|
|
||||||
const requestId = endMatch[1];
|
|
||||||
const request = activeRequests.get(requestId);
|
|
||||||
|
|
||||||
if (request && request.receivedChunks === request.totalChunks) {
|
|
||||||
try {
|
|
||||||
// Reconstruct the base64 string
|
|
||||||
const fullBase64 = request.chunks.join("");
|
|
||||||
verbose &&
|
|
||||||
console.debug(
|
|
||||||
`[dev-fs-plugin] Reconstructed ${request.filename} - base64 length: ${fullBase64.length}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Decode and parse
|
|
||||||
const decodedJson = atob(fullBase64);
|
|
||||||
const writeRequest = JSON.parse(decodedJson);
|
|
||||||
|
|
||||||
if (writeRequest.type === "DEV_FS_WRITE_REQUEST") {
|
|
||||||
verbose &&
|
|
||||||
console.debug(
|
|
||||||
`[dev-fs-plugin] Processing write request for ${writeRequest.filename}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Process the write request
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const fullPath = resolve(projectRoot, writeRequest.filename);
|
|
||||||
verbose &&
|
|
||||||
console.debug(`[dev-fs-plugin] Writing to: ${fullPath}`);
|
|
||||||
await nodeWriteFile(fullPath, writeRequest.data);
|
|
||||||
verbose &&
|
|
||||||
console.debug("[dev-fs-plugin] File written successfully!");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[dev-fs-plugin] Error writing file:", error);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
activeRequests.delete(requestId);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
"[dev-fs-plugin] Error processing chunked request:",
|
|
||||||
String(error),
|
|
||||||
);
|
|
||||||
activeRequests.delete(requestId);
|
|
||||||
}
|
|
||||||
} else if (request) {
|
|
||||||
verbose &&
|
|
||||||
console.debug(
|
|
||||||
`[dev-fs-plugin] Request ${requestId} incomplete: ${request.receivedChunks}/${request.totalChunks} chunks`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
processedProtocolMessage = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we processed any protocol messages, suppress output
|
|
||||||
if (processedProtocolMessage) {
|
|
||||||
return callback ? callback() : true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// @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;
|
|
||||||
if (!isDev) {
|
|
||||||
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 with end delimiter
|
|
||||||
// Base64 encode the JSON to avoid any control character issues
|
|
||||||
const jsonString = JSON.stringify(writeRequest);
|
|
||||||
const encodedJson = btoa(jsonString);
|
|
||||||
|
|
||||||
// Split into reasonable chunks that balance performance vs reliability
|
|
||||||
const chunkSize = 2000; // 2KB chunks - safe for most environments
|
|
||||||
const chunks = [];
|
|
||||||
for (let i = 0; i < encodedJson.length; i += chunkSize) {
|
|
||||||
chunks.push(encodedJson.slice(i, i + chunkSize));
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestId = Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
|
|
||||||
|
|
||||||
// Send start marker (use stdout.write to avoid console display)
|
|
||||||
process.stdout.write('{{DEV_FS_START}} ' + requestId + ' ' + chunks.length + ' ' + filename + '\\n');
|
|
||||||
|
|
||||||
// Send each chunk
|
|
||||||
chunks.forEach((chunk, index) => {
|
|
||||||
process.stdout.write('{{DEV_FS_CHUNK}} ' + requestId + ' ' + index + ' ' + chunk + '\\n');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send end marker
|
|
||||||
process.stdout.write('{{DEV_FS_END}} ' + requestId + '\\n');
|
|
||||||
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}`;
|
|
||||||
return polyfill + code;
|
|
||||||
} else {
|
|
||||||
verbose && console.debug("[dev-fs-plugin] Not transforming", id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
} satisfies Plugin;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+45
-39
@@ -1,26 +1,25 @@
|
|||||||
import {
|
import { config as $config, App, type CreateAppConfig, Connection, guessMimeType } from "bknd";
|
||||||
config as $config,
|
|
||||||
App,
|
|
||||||
type CreateAppConfig,
|
|
||||||
Connection,
|
|
||||||
guessMimeType,
|
|
||||||
type MaybePromise,
|
|
||||||
registries as $registries,
|
|
||||||
} from "bknd";
|
|
||||||
import { $console } from "bknd/utils";
|
import { $console } from "bknd/utils";
|
||||||
import type { Context, MiddlewareHandler, Next } from "hono";
|
import type { Context, MiddlewareHandler, Next } from "hono";
|
||||||
import type { AdminControllerOptions } from "modules/server/AdminController";
|
import type { AdminControllerOptions } from "modules/server/AdminController";
|
||||||
import type { Manifest } from "vite";
|
import type { Manifest } from "vite";
|
||||||
|
|
||||||
export type BkndConfig<Args = any> = CreateAppConfig & {
|
export type BkndConfig<Args = any> = CreateAppConfig & {
|
||||||
app?: Omit<BkndConfig, "app"> | ((args: Args) => MaybePromise<Omit<BkndConfig<Args>, "app">>);
|
app?: CreateAppConfig | ((args: Args) => CreateAppConfig);
|
||||||
onBuilt?: (app: App) => MaybePromise<void>;
|
onBuilt?: (app: App) => Promise<void>;
|
||||||
beforeBuild?: (app?: App, registries?: typeof $registries) => MaybePromise<void>;
|
beforeBuild?: (app: App) => Promise<void>;
|
||||||
buildConfig?: Parameters<App["build"]>[0];
|
buildConfig?: Parameters<App["build"]>[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FrameworkBkndConfig<Args = any> = BkndConfig<Args>;
|
export type FrameworkBkndConfig<Args = any> = BkndConfig<Args>;
|
||||||
|
|
||||||
|
export type CreateAdapterAppOptions = {
|
||||||
|
force?: boolean;
|
||||||
|
id?: string;
|
||||||
|
};
|
||||||
|
export type FrameworkOptions = CreateAdapterAppOptions;
|
||||||
|
export type RuntimeOptions = CreateAdapterAppOptions;
|
||||||
|
|
||||||
export type RuntimeBkndConfig<Args = any> = BkndConfig<Args> & {
|
export type RuntimeBkndConfig<Args = any> = BkndConfig<Args> & {
|
||||||
distPath?: string;
|
distPath?: string;
|
||||||
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
|
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
|
||||||
@@ -31,10 +30,10 @@ export type DefaultArgs = {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function makeConfig<Args = DefaultArgs>(
|
export function makeConfig<Args = DefaultArgs>(
|
||||||
config: BkndConfig<Args>,
|
config: BkndConfig<Args>,
|
||||||
args?: Args,
|
args?: Args,
|
||||||
): Promise<Omit<BkndConfig<Args>, "app">> {
|
): CreateAppConfig {
|
||||||
let additionalConfig: CreateAppConfig = {};
|
let additionalConfig: CreateAppConfig = {};
|
||||||
const { app, ...rest } = config;
|
const { app, ...rest } = config;
|
||||||
if (app) {
|
if (app) {
|
||||||
@@ -42,7 +41,7 @@ export async function makeConfig<Args = DefaultArgs>(
|
|||||||
if (!args) {
|
if (!args) {
|
||||||
throw new Error("args is required when config.app is a function");
|
throw new Error("args is required when config.app is a function");
|
||||||
}
|
}
|
||||||
additionalConfig = await app(args);
|
additionalConfig = app(args);
|
||||||
} else {
|
} else {
|
||||||
additionalConfig = app;
|
additionalConfig = app;
|
||||||
}
|
}
|
||||||
@@ -52,34 +51,41 @@ export async function makeConfig<Args = DefaultArgs>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// a map that contains all apps by id
|
// a map that contains all apps by id
|
||||||
|
const apps = new Map<string, App>();
|
||||||
export async function createAdapterApp<Config extends BkndConfig = BkndConfig, Args = DefaultArgs>(
|
export async function createAdapterApp<Config extends BkndConfig = BkndConfig, Args = DefaultArgs>(
|
||||||
config: Config = {} as Config,
|
config: Config = {} as Config,
|
||||||
args?: Args,
|
args?: Args,
|
||||||
|
opts?: CreateAdapterAppOptions,
|
||||||
): Promise<App> {
|
): Promise<App> {
|
||||||
await config.beforeBuild?.(undefined, $registries);
|
const id = opts?.id ?? "app";
|
||||||
|
let app = apps.get(id);
|
||||||
const appConfig = await makeConfig(config, args);
|
if (!app || opts?.force) {
|
||||||
if (!appConfig.connection || !Connection.isConnection(appConfig.connection)) {
|
const appConfig = makeConfig(config, args);
|
||||||
let connection: Connection | undefined;
|
if (!appConfig.connection || !Connection.isConnection(appConfig.connection)) {
|
||||||
if (Connection.isConnection(config.connection)) {
|
let connection: Connection | undefined;
|
||||||
connection = config.connection;
|
if (Connection.isConnection(config.connection)) {
|
||||||
} else {
|
connection = config.connection;
|
||||||
const sqlite = (await import("bknd/adapter/sqlite")).sqlite;
|
} else {
|
||||||
const conf = appConfig.connection ?? { url: ":memory:" };
|
const sqlite = (await import("bknd/adapter/sqlite")).sqlite;
|
||||||
connection = sqlite(conf) as any;
|
const conf = appConfig.connection ?? { url: ":memory:" };
|
||||||
$console.info(`Using ${connection!.name} connection`, conf.url);
|
connection = sqlite(conf);
|
||||||
|
$console.info(`Using ${connection!.name} connection`, conf.url);
|
||||||
|
}
|
||||||
|
appConfig.connection = connection;
|
||||||
}
|
}
|
||||||
appConfig.connection = connection;
|
|
||||||
}
|
|
||||||
|
|
||||||
return App.create(appConfig);
|
app = App.create(appConfig);
|
||||||
|
apps.set(id, app);
|
||||||
|
}
|
||||||
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createFrameworkApp<Args = DefaultArgs>(
|
export async function createFrameworkApp<Args = DefaultArgs>(
|
||||||
config: FrameworkBkndConfig = {},
|
config: FrameworkBkndConfig = {},
|
||||||
args?: Args,
|
args?: Args,
|
||||||
|
opts?: FrameworkOptions,
|
||||||
): Promise<App> {
|
): Promise<App> {
|
||||||
const app = await createAdapterApp(config, args);
|
const app = await createAdapterApp(config, args, opts);
|
||||||
|
|
||||||
if (!app.isBuilt()) {
|
if (!app.isBuilt()) {
|
||||||
if (config.onBuilt) {
|
if (config.onBuilt) {
|
||||||
@@ -92,7 +98,7 @@ export async function createFrameworkApp<Args = DefaultArgs>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await config.beforeBuild?.(app, $registries);
|
await config.beforeBuild?.(app);
|
||||||
await app.build(config.buildConfig);
|
await app.build(config.buildConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,8 +108,9 @@ export async function createFrameworkApp<Args = DefaultArgs>(
|
|||||||
export async function createRuntimeApp<Args = DefaultArgs>(
|
export async function createRuntimeApp<Args = DefaultArgs>(
|
||||||
{ serveStatic, adminOptions, ...config }: RuntimeBkndConfig<Args> = {},
|
{ serveStatic, adminOptions, ...config }: RuntimeBkndConfig<Args> = {},
|
||||||
args?: Args,
|
args?: Args,
|
||||||
|
opts?: RuntimeOptions,
|
||||||
): Promise<App> {
|
): Promise<App> {
|
||||||
const app = await createAdapterApp(config, args);
|
const app = await createAdapterApp(config, args, opts);
|
||||||
|
|
||||||
if (!app.isBuilt()) {
|
if (!app.isBuilt()) {
|
||||||
app.emgr.onEvent(
|
app.emgr.onEvent(
|
||||||
@@ -124,7 +131,7 @@ export async function createRuntimeApp<Args = DefaultArgs>(
|
|||||||
"sync",
|
"sync",
|
||||||
);
|
);
|
||||||
|
|
||||||
await config.beforeBuild?.(app, $registries);
|
await config.beforeBuild?.(app);
|
||||||
await app.build(config.buildConfig);
|
await app.build(config.buildConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,17 +161,16 @@ export function serveStaticViaImport(opts?: { manifest?: Manifest }) {
|
|||||||
return async (c: Context, next: Next) => {
|
return async (c: Context, next: Next) => {
|
||||||
if (!files) {
|
if (!files) {
|
||||||
const manifest =
|
const manifest =
|
||||||
opts?.manifest ||
|
opts?.manifest || ((await import("bknd/dist/manifest.json")).default as Manifest);
|
||||||
((await import("bknd/dist/manifest.json", { with: { type: "json" } }))
|
|
||||||
.default as Manifest);
|
|
||||||
files = Object.values(manifest).flatMap((asset) => [asset.file, ...(asset.css || [])]);
|
files = Object.values(manifest).flatMap((asset) => [asset.file, ...(asset.css || [])]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = c.req.path.substring(1);
|
const path = c.req.path.substring(1);
|
||||||
if (files.includes(path)) {
|
if (files.includes(path)) {
|
||||||
try {
|
try {
|
||||||
const content = await import(/* @vite-ignore */ `bknd/static/${path}?raw`, {
|
const content = await import(`bknd/static/${path}?raw`, {
|
||||||
with: { type: "text" },
|
/* @vite-ignore */
|
||||||
|
assert: { type: "text" },
|
||||||
}).then((m) => m.default);
|
}).then((m) => m.default);
|
||||||
|
|
||||||
if (content) {
|
if (content) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createFrameworkApp, type FrameworkBkndConfig } from "bknd/adapter";
|
import { createFrameworkApp, type FrameworkBkndConfig, type FrameworkOptions } from "bknd/adapter";
|
||||||
import { isNode } from "bknd/utils";
|
import { isNode } from "bknd/utils";
|
||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
@@ -10,8 +10,9 @@ export type NextjsBkndConfig<Env = NextjsEnv> = FrameworkBkndConfig<Env> & {
|
|||||||
export async function getApp<Env = NextjsEnv>(
|
export async function getApp<Env = NextjsEnv>(
|
||||||
config: NextjsBkndConfig<Env>,
|
config: NextjsBkndConfig<Env>,
|
||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
|
opts?: FrameworkOptions,
|
||||||
) {
|
) {
|
||||||
return await createFrameworkApp(config, args ?? (process.env as Env));
|
return await createFrameworkApp(config, args ?? (process.env as Env), opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCleanRequest(req: Request, cleanRequest: NextjsBkndConfig["cleanRequest"]) {
|
function getCleanRequest(req: Request, cleanRequest: NextjsBkndConfig["cleanRequest"]) {
|
||||||
@@ -40,9 +41,10 @@ function getCleanRequest(req: Request, cleanRequest: NextjsBkndConfig["cleanRequ
|
|||||||
export function serve<Env = NextjsEnv>(
|
export function serve<Env = NextjsEnv>(
|
||||||
{ cleanRequest, ...config }: NextjsBkndConfig<Env> = {},
|
{ cleanRequest, ...config }: NextjsBkndConfig<Env> = {},
|
||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
|
opts?: FrameworkOptions,
|
||||||
) {
|
) {
|
||||||
return async (req: Request) => {
|
return async (req: Request) => {
|
||||||
const app = await getApp(config, args);
|
const app = await getApp(config, args, opts);
|
||||||
const request = getCleanRequest(req, cleanRequest);
|
const request = getCleanRequest(req, cleanRequest);
|
||||||
return app.fetch(request);
|
return app.fetch(request);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,29 +1,19 @@
|
|||||||
import {
|
import { genericSqlite } from "bknd";
|
||||||
genericSqlite,
|
|
||||||
type GenericSqliteConnection,
|
|
||||||
type GenericSqliteConnectionConfig,
|
|
||||||
} from "bknd";
|
|
||||||
import { DatabaseSync } from "node:sqlite";
|
import { DatabaseSync } from "node:sqlite";
|
||||||
import { omitKeys } from "bknd/utils";
|
|
||||||
|
|
||||||
export type NodeSqliteConnection = GenericSqliteConnection<DatabaseSync>;
|
export type NodeSqliteConnectionConfig = {
|
||||||
export type NodeSqliteConnectionConfig = Omit<
|
database: DatabaseSync;
|
||||||
GenericSqliteConnectionConfig<DatabaseSync>,
|
};
|
||||||
"name" | "supports"
|
|
||||||
> &
|
|
||||||
({ database?: DatabaseSync; url?: never } | { url?: string; database?: never });
|
|
||||||
|
|
||||||
export function nodeSqlite(config?: NodeSqliteConnectionConfig) {
|
export function nodeSqlite(config?: NodeSqliteConnectionConfig | { url: string }) {
|
||||||
let db: DatabaseSync | undefined;
|
let db: DatabaseSync;
|
||||||
if (config) {
|
if (config) {
|
||||||
if ("database" in config && config.database) {
|
if ("database" in config) {
|
||||||
db = config.database;
|
db = config.database;
|
||||||
} else if (config.url) {
|
} else {
|
||||||
db = new DatabaseSync(config.url);
|
db = new DatabaseSync(config.url);
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
|
||||||
if (!db) {
|
|
||||||
db = new DatabaseSync(":memory:");
|
db = new DatabaseSync(":memory:");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +21,11 @@ export function nodeSqlite(config?: NodeSqliteConnectionConfig) {
|
|||||||
"node-sqlite",
|
"node-sqlite",
|
||||||
db,
|
db,
|
||||||
(utils) => {
|
(utils) => {
|
||||||
const getStmt = (sql: string) => db.prepare(sql);
|
const getStmt = (sql: string) => {
|
||||||
|
const stmt = db.prepare(sql);
|
||||||
|
//stmt.setReadBigInts(true);
|
||||||
|
return stmt;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
db,
|
db,
|
||||||
@@ -55,7 +49,6 @@ export function nodeSqlite(config?: NodeSqliteConnectionConfig) {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...omitKeys(config ?? ({} as any), ["database", "url", "name", "supports"]),
|
|
||||||
supports: {
|
supports: {
|
||||||
batching: false,
|
batching: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
import { nodeSqlite } from "./NodeSqliteConnection";
|
import { nodeSqlite } from "./NodeSqliteConnection";
|
||||||
import { DatabaseSync } from "node:sqlite";
|
import { DatabaseSync } from "node:sqlite";
|
||||||
import { connectionTestSuite } from "data/connection/connection-test-suite";
|
import { connectionTestSuite } from "data/connection/connection-test-suite";
|
||||||
import { describe, beforeAll, afterAll, test, expect, vi } from "vitest";
|
import { describe } from "vitest";
|
||||||
import { viTestRunner } from "../vitest";
|
import { viTestRunner } from "../vitest";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
import { GenericSqliteConnection } from "data/connection/sqlite/GenericSqliteConnection";
|
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
afterAll(() => enableConsoleLog());
|
|
||||||
|
|
||||||
describe("NodeSqliteConnection", () => {
|
describe("NodeSqliteConnection", () => {
|
||||||
connectionTestSuite(viTestRunner, {
|
connectionTestSuite(viTestRunner, {
|
||||||
@@ -17,20 +12,4 @@ describe("NodeSqliteConnection", () => {
|
|||||||
}),
|
}),
|
||||||
rawDialectDetails: [],
|
rawDialectDetails: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
test("onCreateConnection", async () => {
|
|
||||||
const called = vi.fn(() => null);
|
|
||||||
|
|
||||||
const conn = nodeSqlite({
|
|
||||||
onCreateConnection: (db) => {
|
|
||||||
expect(db).toBeInstanceOf(DatabaseSync);
|
|
||||||
called();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await conn.ping();
|
|
||||||
|
|
||||||
expect(conn).toBeInstanceOf(GenericSqliteConnection);
|
|
||||||
expect(conn.db).toBeInstanceOf(DatabaseSync);
|
|
||||||
expect(called).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import path from "node:path";
|
|||||||
import { serve as honoServe } from "@hono/node-server";
|
import { serve as honoServe } from "@hono/node-server";
|
||||||
import { serveStatic } from "@hono/node-server/serve-static";
|
import { serveStatic } from "@hono/node-server/serve-static";
|
||||||
import { registerLocalMediaAdapter } from "adapter/node/storage";
|
import { registerLocalMediaAdapter } from "adapter/node/storage";
|
||||||
import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter";
|
import { type RuntimeBkndConfig, createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
|
||||||
import { config as $config, type App } from "bknd";
|
import { config as $config, type App } from "bknd";
|
||||||
import { $console } from "bknd/utils";
|
import { $console } from "bknd/utils";
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ export type NodeBkndConfig<Env = NodeEnv> = RuntimeBkndConfig<Env> & {
|
|||||||
export async function createApp<Env = NodeEnv>(
|
export async function createApp<Env = NodeEnv>(
|
||||||
{ distPath, relativeDistPath, ...config }: NodeBkndConfig<Env> = {},
|
{ distPath, relativeDistPath, ...config }: NodeBkndConfig<Env> = {},
|
||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
|
opts?: RuntimeOptions,
|
||||||
) {
|
) {
|
||||||
const root = path.relative(
|
const root = path.relative(
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
@@ -35,17 +36,19 @@ export async function createApp<Env = NodeEnv>(
|
|||||||
},
|
},
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
args ?? { env: process.env },
|
args ?? { env: process.env },
|
||||||
|
opts,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createHandler<Env = NodeEnv>(
|
export function createHandler<Env = NodeEnv>(
|
||||||
config: NodeBkndConfig<Env> = {},
|
config: NodeBkndConfig<Env> = {},
|
||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
|
opts?: RuntimeOptions,
|
||||||
) {
|
) {
|
||||||
let app: App | undefined;
|
let app: App | undefined;
|
||||||
return async (req: Request) => {
|
return async (req: Request) => {
|
||||||
if (!app) {
|
if (!app) {
|
||||||
app = await createApp(config, args ?? (process.env as Env));
|
app = await createApp(config, args ?? (process.env as Env), opts);
|
||||||
}
|
}
|
||||||
return app.fetch(req);
|
return app.fetch(req);
|
||||||
};
|
};
|
||||||
@@ -54,12 +57,13 @@ export function createHandler<Env = NodeEnv>(
|
|||||||
export function serve<Env = NodeEnv>(
|
export function serve<Env = NodeEnv>(
|
||||||
{ port = $config.server.default_port, hostname, listener, ...config }: NodeBkndConfig<Env> = {},
|
{ port = $config.server.default_port, hostname, listener, ...config }: NodeBkndConfig<Env> = {},
|
||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
|
opts?: RuntimeOptions,
|
||||||
) {
|
) {
|
||||||
honoServe(
|
honoServe(
|
||||||
{
|
{
|
||||||
port,
|
port,
|
||||||
hostname,
|
hostname,
|
||||||
fetch: createHandler(config, args),
|
fetch: createHandler(config, args, opts),
|
||||||
},
|
},
|
||||||
(connInfo) => {
|
(connInfo) => {
|
||||||
$console.log(`Server is running on http://localhost:${connInfo.port}`);
|
$console.log(`Server is running on http://localhost:${connInfo.port}`);
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import { describe, beforeAll, afterAll } from "vitest";
|
|||||||
import * as node from "./node.adapter";
|
import * as node from "./node.adapter";
|
||||||
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||||
import { viTestRunner } from "adapter/node/vitest";
|
import { viTestRunner } from "adapter/node/vitest";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||||
|
|
||||||
|
beforeAll(() => disableConsoleLog());
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
describe("node adapter", () => {
|
describe("node adapter", () => {
|
||||||
adapterTestSuite(viTestRunner, {
|
adapterTestSuite(viTestRunner, {
|
||||||
|
|||||||
@@ -80,79 +80,18 @@ export class StorageLocalAdapter extends StorageAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseRangeHeader(
|
|
||||||
rangeHeader: string,
|
|
||||||
fileSize: number,
|
|
||||||
): { start: number; end: number } | null {
|
|
||||||
// Parse "bytes=start-end" format
|
|
||||||
const match = rangeHeader.match(/^bytes=(\d*)-(\d*)$/);
|
|
||||||
if (!match) return null;
|
|
||||||
|
|
||||||
const [, startStr, endStr] = match;
|
|
||||||
let start = startStr ? Number.parseInt(startStr, 10) : 0;
|
|
||||||
let end = endStr ? Number.parseInt(endStr, 10) : fileSize - 1;
|
|
||||||
|
|
||||||
// Handle suffix-byte-range-spec (e.g., "bytes=-500")
|
|
||||||
if (!startStr && endStr) {
|
|
||||||
start = Math.max(0, fileSize - Number.parseInt(endStr, 10));
|
|
||||||
end = fileSize - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate range
|
|
||||||
if (start < 0 || end >= fileSize || start > end) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { start, end };
|
|
||||||
}
|
|
||||||
|
|
||||||
async getObject(key: string, headers: Headers): Promise<Response> {
|
async getObject(key: string, headers: Headers): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const filePath = `${this.config.path}/${key}`;
|
const content = await readFile(`${this.config.path}/${key}`);
|
||||||
const stats = await stat(filePath);
|
|
||||||
const fileSize = stats.size;
|
|
||||||
const mimeType = guessMimeType(key);
|
const mimeType = guessMimeType(key);
|
||||||
|
|
||||||
const responseHeaders = new Headers({
|
return new Response(content, {
|
||||||
"Accept-Ranges": "bytes",
|
status: 200,
|
||||||
"Content-Type": mimeType || "application/octet-stream",
|
headers: {
|
||||||
|
"Content-Type": mimeType || "application/octet-stream",
|
||||||
|
"Content-Length": content.length.toString(),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const rangeHeader = headers.get("range");
|
|
||||||
|
|
||||||
if (rangeHeader) {
|
|
||||||
const range = this.parseRangeHeader(rangeHeader, fileSize);
|
|
||||||
|
|
||||||
if (!range) {
|
|
||||||
// Invalid range - return 416 Range Not Satisfiable
|
|
||||||
responseHeaders.set("Content-Range", `bytes */${fileSize}`);
|
|
||||||
return new Response("", {
|
|
||||||
status: 416,
|
|
||||||
headers: responseHeaders,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { start, end } = range;
|
|
||||||
const content = await readFile(filePath, { encoding: null });
|
|
||||||
const chunk = content.slice(start, end + 1);
|
|
||||||
|
|
||||||
responseHeaders.set("Content-Range", `bytes ${start}-${end}/${fileSize}`);
|
|
||||||
responseHeaders.set("Content-Length", chunk.length.toString());
|
|
||||||
|
|
||||||
return new Response(chunk, {
|
|
||||||
status: 206, // Partial Content
|
|
||||||
headers: responseHeaders,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Normal request - return entire file
|
|
||||||
const content = await readFile(filePath);
|
|
||||||
responseHeaders.set("Content-Length", content.length.toString());
|
|
||||||
|
|
||||||
return new Response(content, {
|
|
||||||
status: 200,
|
|
||||||
headers: responseHeaders,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle file reading errors
|
// Handle file reading errors
|
||||||
return new Response("", { status: 404 });
|
return new Response("", { status: 404 });
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import { describe, beforeAll, afterAll } from "vitest";
|
import { describe } from "vitest";
|
||||||
import { viTestRunner } from "adapter/node/vitest";
|
import { viTestRunner } from "adapter/node/vitest";
|
||||||
import { StorageLocalAdapter } from "adapter/node";
|
import { StorageLocalAdapter } from "adapter/node";
|
||||||
import { adapterTestSuite } from "media/storage/adapters/adapter-test-suite";
|
import { adapterTestSuite } from "media/storage/adapters/adapter-test-suite";
|
||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
afterAll(() => enableConsoleLog());
|
|
||||||
|
|
||||||
describe("StorageLocalAdapter (node)", async () => {
|
describe("StorageLocalAdapter (node)", async () => {
|
||||||
const basePath = path.resolve(import.meta.dirname, "../../../../__test__/_assets");
|
const basePath = path.resolve(import.meta.dirname, "../../../../__test__/_assets");
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import nodeAssert from "node:assert/strict";
|
import nodeAssert from "node:assert/strict";
|
||||||
import { test, describe, beforeEach, afterEach, after, before } from "node:test";
|
import { test, describe, beforeEach, afterEach } from "node:test";
|
||||||
import type { Matcher, Test, TestFn, TestRunner } from "core/test";
|
import type { Matcher, Test, TestFn, TestRunner } from "core/test";
|
||||||
|
|
||||||
// Track mock function calls
|
// Track mock function calls
|
||||||
@@ -99,6 +99,5 @@ export const nodeTestRunner: TestRunner = {
|
|||||||
}),
|
}),
|
||||||
beforeEach: beforeEach,
|
beforeEach: beforeEach,
|
||||||
afterEach: afterEach,
|
afterEach: afterEach,
|
||||||
afterAll: after,
|
afterAll: () => {},
|
||||||
beforeAll: before,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { TestFn, TestRunner, Test } from "core/test";
|
import type { TestFn, TestRunner, Test } from "core/test";
|
||||||
import { describe, test, expect, vi, beforeEach, afterEach, afterAll, beforeAll } from "vitest";
|
import { describe, test, expect, vi, beforeEach, afterEach, afterAll } from "vitest";
|
||||||
|
|
||||||
function vitestTest(label: string, fn: TestFn, options?: any) {
|
function vitestTest(label: string, fn: TestFn, options?: any) {
|
||||||
return test(label, fn as any);
|
return test(label, fn as any);
|
||||||
@@ -50,5 +50,4 @@ export const viTestRunner: TestRunner = {
|
|||||||
beforeEach: beforeEach,
|
beforeEach: beforeEach,
|
||||||
afterEach: afterEach,
|
afterEach: afterEach,
|
||||||
afterAll: afterAll,
|
afterAll: afterAll,
|
||||||
beforeAll: beforeAll,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ afterAll(enableConsoleLog);
|
|||||||
describe("react-router adapter", () => {
|
describe("react-router adapter", () => {
|
||||||
adapterTestSuite(bunTestRunner, {
|
adapterTestSuite(bunTestRunner, {
|
||||||
makeApp: rr.getApp,
|
makeApp: rr.getApp,
|
||||||
makeHandler: (c, a) => (request: Request) => rr.serve(c, a?.env)({ request }),
|
makeHandler: (c, a, o) => (request: Request) => rr.serve(c, a?.env, o)({ request }),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter";
|
import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter";
|
||||||
|
import type { FrameworkOptions } from "adapter";
|
||||||
|
|
||||||
type ReactRouterEnv = NodeJS.ProcessEnv;
|
type ReactRouterEnv = NodeJS.ProcessEnv;
|
||||||
type ReactRouterFunctionArgs = {
|
type ReactRouterFunctionArgs = {
|
||||||
@@ -9,15 +10,17 @@ export type ReactRouterBkndConfig<Env = ReactRouterEnv> = FrameworkBkndConfig<En
|
|||||||
export async function getApp<Env = ReactRouterEnv>(
|
export async function getApp<Env = ReactRouterEnv>(
|
||||||
config: ReactRouterBkndConfig<Env>,
|
config: ReactRouterBkndConfig<Env>,
|
||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
|
opts?: FrameworkOptions,
|
||||||
) {
|
) {
|
||||||
return await createFrameworkApp(config, args ?? process.env);
|
return await createFrameworkApp(config, args ?? process.env, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serve<Env = ReactRouterEnv>(
|
export function serve<Env = ReactRouterEnv>(
|
||||||
config: ReactRouterBkndConfig<Env> = {},
|
config: ReactRouterBkndConfig<Env> = {},
|
||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
|
opts?: FrameworkOptions,
|
||||||
) {
|
) {
|
||||||
return async (fnArgs: ReactRouterFunctionArgs) => {
|
return async (fnArgs: ReactRouterFunctionArgs) => {
|
||||||
return (await getApp(config, args)).fetch(fnArgs.request);
|
return (await getApp(config, args, opts)).fetch(fnArgs.request);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { serveStatic } from "@hono/node-server/serve-static";
|
import { serveStatic } from "@hono/node-server/serve-static";
|
||||||
import { type DevServerOptions, default as honoViteDevServer } from "@hono/vite-dev-server";
|
import { type DevServerOptions, default as honoViteDevServer } from "@hono/vite-dev-server";
|
||||||
import type { App } from "bknd";
|
import type { App } from "bknd";
|
||||||
import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter";
|
import { type RuntimeBkndConfig, createRuntimeApp, type FrameworkOptions } from "bknd/adapter";
|
||||||
import { registerLocalMediaAdapter } from "bknd/adapter/node";
|
import { registerLocalMediaAdapter } from "bknd/adapter/node";
|
||||||
import { devServerConfig } from "./dev-server-config";
|
import { devServerConfig } from "./dev-server-config";
|
||||||
import type { MiddlewareHandler } from "hono";
|
import type { MiddlewareHandler } from "hono";
|
||||||
@@ -30,6 +30,7 @@ ${addBkndContext ? "<!-- BKND_CONTEXT -->" : ""}
|
|||||||
async function createApp<ViteEnv>(
|
async function createApp<ViteEnv>(
|
||||||
config: ViteBkndConfig<ViteEnv> = {},
|
config: ViteBkndConfig<ViteEnv> = {},
|
||||||
env: ViteEnv = {} as ViteEnv,
|
env: ViteEnv = {} as ViteEnv,
|
||||||
|
opts: FrameworkOptions = {},
|
||||||
): Promise<App> {
|
): Promise<App> {
|
||||||
registerLocalMediaAdapter();
|
registerLocalMediaAdapter();
|
||||||
return await createRuntimeApp(
|
return await createRuntimeApp(
|
||||||
@@ -46,13 +47,18 @@ async function createApp<ViteEnv>(
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
env,
|
env,
|
||||||
|
opts,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serve<ViteEnv>(config: ViteBkndConfig<ViteEnv> = {}, args?: ViteEnv) {
|
export function serve<ViteEnv>(
|
||||||
|
config: ViteBkndConfig<ViteEnv> = {},
|
||||||
|
args?: ViteEnv,
|
||||||
|
opts?: FrameworkOptions,
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
async fetch(request: Request, env: any, ctx: ExecutionContext) {
|
async fetch(request: Request, env: any, ctx: ExecutionContext) {
|
||||||
const app = await createApp(config, env);
|
const app = await createApp(config, env, opts);
|
||||||
return app.fetch(request, env, ctx);
|
return app.fetch(request, env, ctx);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+2
-30
@@ -1,8 +1,8 @@
|
|||||||
import type { DB, PrimaryFieldType } from "bknd";
|
import type { DB } from "bknd";
|
||||||
import * as AuthPermissions from "auth/auth-permissions";
|
import * as AuthPermissions from "auth/auth-permissions";
|
||||||
import type { AuthStrategy } from "auth/authenticate/strategies/Strategy";
|
import type { AuthStrategy } from "auth/authenticate/strategies/Strategy";
|
||||||
import type { PasswordStrategy } from "auth/authenticate/strategies/PasswordStrategy";
|
import type { PasswordStrategy } from "auth/authenticate/strategies/PasswordStrategy";
|
||||||
import { $console, secureRandomString, transformObject } from "bknd/utils";
|
import { $console, secureRandomString, transformObject } from "core/utils";
|
||||||
import type { Entity, EntityManager } from "data/entities";
|
import type { Entity, EntityManager } from "data/entities";
|
||||||
import { em, entity, enumm, type FieldSchema } from "data/prototype";
|
import { em, entity, enumm, type FieldSchema } from "data/prototype";
|
||||||
import { Module } from "modules/Module";
|
import { Module } from "modules/Module";
|
||||||
@@ -14,7 +14,6 @@ import { usersFields } from "./auth-entities";
|
|||||||
import { Authenticator } from "./authenticate/Authenticator";
|
import { Authenticator } from "./authenticate/Authenticator";
|
||||||
import { Role } from "./authorize/Role";
|
import { Role } from "./authorize/Role";
|
||||||
|
|
||||||
export type UsersFields = typeof AppAuth.usersFields;
|
|
||||||
export type UserFieldSchema = FieldSchema<typeof AppAuth.usersFields>;
|
export type UserFieldSchema = FieldSchema<typeof AppAuth.usersFields>;
|
||||||
declare module "bknd" {
|
declare module "bknd" {
|
||||||
interface Users extends AppEntity, UserFieldSchema {}
|
interface Users extends AppEntity, UserFieldSchema {}
|
||||||
@@ -88,7 +87,6 @@ export class AppAuth extends Module<AppAuthSchema> {
|
|||||||
super.setBuilt();
|
super.setBuilt();
|
||||||
|
|
||||||
this._controller = new AuthController(this);
|
this._controller = new AuthController(this);
|
||||||
this._controller.registerMcp();
|
|
||||||
this.ctx.server.route(this.config.basepath, this._controller.getController());
|
this.ctx.server.route(this.config.basepath, this._controller.getController());
|
||||||
this.ctx.guard.registerPermissions(AuthPermissions);
|
this.ctx.guard.registerPermissions(AuthPermissions);
|
||||||
}
|
}
|
||||||
@@ -178,32 +176,6 @@ export class AppAuth extends Module<AppAuthSchema> {
|
|||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
|
|
||||||
async changePassword(userId: PrimaryFieldType, newPassword: string) {
|
|
||||||
const users_entity = this.config.entity_name as "users";
|
|
||||||
const { data: user } = await this.em.repository(users_entity).findId(userId);
|
|
||||||
if (!user) {
|
|
||||||
throw new Error("User not found");
|
|
||||||
} else if (user.strategy !== "password") {
|
|
||||||
throw new Error("User is not using password strategy");
|
|
||||||
}
|
|
||||||
|
|
||||||
const togglePw = (visible: boolean) => {
|
|
||||||
const field = this.em.entity(users_entity).field("strategy_value")!;
|
|
||||||
|
|
||||||
field.config.hidden = !visible;
|
|
||||||
field.config.fillable = visible;
|
|
||||||
};
|
|
||||||
|
|
||||||
const pw = this.authenticator.strategy("password" as const) as PasswordStrategy;
|
|
||||||
togglePw(true);
|
|
||||||
await this.em.mutator(users_entity).updateOne(user.id, {
|
|
||||||
strategy_value: await pw.hash(newPassword),
|
|
||||||
});
|
|
||||||
togglePw(false);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
override toJSON(secrets?: boolean): AppAuthSchema {
|
override toJSON(secrets?: boolean): AppAuthSchema {
|
||||||
if (!this.config.enabled) {
|
if (!this.config.enabled) {
|
||||||
return this.configDefault;
|
return this.configDefault;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AppAuth } from "auth/AppAuth";
|
import { AppAuth } from "auth/AppAuth";
|
||||||
import type { CreateUser, SafeUser, User, UserPool } from "auth/authenticate/Authenticator";
|
import type { CreateUser, SafeUser, User, UserPool } from "auth/authenticate/Authenticator";
|
||||||
import { $console } from "bknd/utils";
|
import { $console } from "core/utils";
|
||||||
import { pick } from "lodash-es";
|
import { pick } from "lodash-es";
|
||||||
import {
|
import {
|
||||||
InvalidConditionsException,
|
InvalidConditionsException,
|
||||||
|
|||||||
@@ -1,20 +1,11 @@
|
|||||||
import type { DB, SafeUser } from "bknd";
|
import type { SafeUser } from "bknd";
|
||||||
import type { AuthStrategy } from "auth/authenticate/strategies/Strategy";
|
import type { AuthStrategy } from "auth/authenticate/strategies/Strategy";
|
||||||
import type { AppAuth } from "auth/AppAuth";
|
import type { AppAuth } from "auth/AppAuth";
|
||||||
import * as AuthPermissions from "auth/auth-permissions";
|
import * as AuthPermissions from "auth/auth-permissions";
|
||||||
import * as DataPermissions from "data/permissions";
|
import * as DataPermissions from "data/permissions";
|
||||||
import type { Hono } from "hono";
|
import type { Hono } from "hono";
|
||||||
import { Controller, type ServerEnv } from "modules/Controller";
|
import { Controller, type ServerEnv } from "modules/Controller";
|
||||||
import {
|
import { describeRoute, jsc, s, parse, InvalidSchemaError, transformObject } from "bknd/utils";
|
||||||
describeRoute,
|
|
||||||
jsc,
|
|
||||||
s,
|
|
||||||
parse,
|
|
||||||
InvalidSchemaError,
|
|
||||||
transformObject,
|
|
||||||
mcpTool,
|
|
||||||
} from "bknd/utils";
|
|
||||||
import type { PasswordStrategy } from "auth/authenticate/strategies";
|
|
||||||
|
|
||||||
export type AuthActionResponse = {
|
export type AuthActionResponse = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -127,9 +118,6 @@ export class AuthController extends Controller {
|
|||||||
summary: "Get the current user",
|
summary: "Get the current user",
|
||||||
tags: ["auth"],
|
tags: ["auth"],
|
||||||
}),
|
}),
|
||||||
mcpTool("auth_me", {
|
|
||||||
noErrorCodes: [403],
|
|
||||||
}),
|
|
||||||
auth(),
|
auth(),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const claims = c.get("auth")?.user;
|
const claims = c.get("auth")?.user;
|
||||||
@@ -171,7 +159,6 @@ export class AuthController extends Controller {
|
|||||||
summary: "Get the available authentication strategies",
|
summary: "Get the available authentication strategies",
|
||||||
tags: ["auth"],
|
tags: ["auth"],
|
||||||
}),
|
}),
|
||||||
mcpTool("auth_strategies"),
|
|
||||||
jsc("query", s.object({ include_disabled: s.boolean().optional() })),
|
jsc("query", s.object({ include_disabled: s.boolean().optional() })),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { include_disabled } = c.req.valid("query");
|
const { include_disabled } = c.req.valid("query");
|
||||||
@@ -201,120 +188,4 @@ export class AuthController extends Controller {
|
|||||||
|
|
||||||
return hono;
|
return hono;
|
||||||
}
|
}
|
||||||
|
|
||||||
override registerMcp(): void {
|
|
||||||
const { mcp } = this.auth.ctx;
|
|
||||||
const idType = s.anyOf([s.number({ title: "Integer" }), s.string({ title: "UUID" })]);
|
|
||||||
|
|
||||||
const getUser = async (params: { id?: string | number; email?: string }) => {
|
|
||||||
let user: DB["users"] | undefined = undefined;
|
|
||||||
if (params.id) {
|
|
||||||
const { data } = await this.userRepo.findId(params.id);
|
|
||||||
user = data;
|
|
||||||
} else if (params.email) {
|
|
||||||
const { data } = await this.userRepo.findOne({ email: params.email });
|
|
||||||
user = data;
|
|
||||||
}
|
|
||||||
if (!user) {
|
|
||||||
throw new Error("User not found");
|
|
||||||
}
|
|
||||||
return user;
|
|
||||||
};
|
|
||||||
|
|
||||||
const roles = Object.keys(this.auth.config.roles ?? {});
|
|
||||||
mcp.tool(
|
|
||||||
// @todo: needs permission
|
|
||||||
"auth_user_create",
|
|
||||||
{
|
|
||||||
description: "Create a new user",
|
|
||||||
inputSchema: s.object({
|
|
||||||
email: s.string({ format: "email" }),
|
|
||||||
password: s.string({ minLength: 8 }),
|
|
||||||
role: s
|
|
||||||
.string({
|
|
||||||
enum: roles.length > 0 ? roles : undefined,
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
async (params, c) => {
|
|
||||||
await c.context.ctx().helper.throwUnlessGranted(AuthPermissions.createUser, c);
|
|
||||||
|
|
||||||
return c.json(await this.auth.createUser(params));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
mcp.tool(
|
|
||||||
// @todo: needs permission
|
|
||||||
"auth_user_token",
|
|
||||||
{
|
|
||||||
description: "Get a user token",
|
|
||||||
inputSchema: s.object({
|
|
||||||
id: idType.optional(),
|
|
||||||
email: s.string({ format: "email" }).optional(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
async (params, c) => {
|
|
||||||
await c.context.ctx().helper.throwUnlessGranted(AuthPermissions.createToken, c);
|
|
||||||
|
|
||||||
const user = await getUser(params);
|
|
||||||
return c.json({ user, token: await this.auth.authenticator.jwt(user) });
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
mcp.tool(
|
|
||||||
// @todo: needs permission
|
|
||||||
"auth_user_password_change",
|
|
||||||
{
|
|
||||||
description: "Change a user's password",
|
|
||||||
inputSchema: s.object({
|
|
||||||
id: idType.optional(),
|
|
||||||
email: s.string({ format: "email" }).optional(),
|
|
||||||
password: s.string({ minLength: 8 }),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
async (params, c) => {
|
|
||||||
await c.context.ctx().helper.throwUnlessGranted(AuthPermissions.changePassword, c);
|
|
||||||
|
|
||||||
const user = await getUser(params);
|
|
||||||
if (!(await this.auth.changePassword(user.id, params.password))) {
|
|
||||||
throw new Error("Failed to change password");
|
|
||||||
}
|
|
||||||
return c.json({ changed: true });
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
mcp.tool(
|
|
||||||
// @todo: needs permission
|
|
||||||
"auth_user_password_test",
|
|
||||||
{
|
|
||||||
description: "Test a user's password",
|
|
||||||
inputSchema: s.object({
|
|
||||||
email: s.string({ format: "email" }),
|
|
||||||
password: s.string({ minLength: 8 }),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
async (params, c) => {
|
|
||||||
await c.context.ctx().helper.throwUnlessGranted(AuthPermissions.testPassword, c);
|
|
||||||
|
|
||||||
const pw = this.auth.authenticator.strategy("password") as PasswordStrategy;
|
|
||||||
const controller = pw.getController(this.auth.authenticator);
|
|
||||||
|
|
||||||
const res = await controller.request(
|
|
||||||
new Request("https://localhost/login", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: params.email,
|
|
||||||
password: params.password,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return c.json({ valid: res.ok });
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,3 @@ import { Permission } from "core/security/Permission";
|
|||||||
|
|
||||||
export const createUser = new Permission("auth.user.create");
|
export const createUser = new Permission("auth.user.create");
|
||||||
//export const updateUser = new Permission("auth.user.update");
|
//export const updateUser = new Permission("auth.user.update");
|
||||||
export const testPassword = new Permission("auth.user.password.test");
|
|
||||||
export const changePassword = new Permission("auth.user.password.change");
|
|
||||||
export const createToken = new Permission("auth.user.token.create");
|
|
||||||
|
|||||||
+11
-22
@@ -1,7 +1,6 @@
|
|||||||
import { cookieConfig, jwtConfig } from "auth/authenticate/Authenticator";
|
import { cookieConfig, jwtConfig } from "auth/authenticate/Authenticator";
|
||||||
import { CustomOAuthStrategy, OAuthStrategy, PasswordStrategy } from "auth/authenticate/strategies";
|
import { CustomOAuthStrategy, OAuthStrategy, PasswordStrategy } from "auth/authenticate/strategies";
|
||||||
import { objectTransform, s } from "bknd/utils";
|
import { objectTransform, s } from "bknd/utils";
|
||||||
import { $object, $record } from "modules/mcp";
|
|
||||||
|
|
||||||
export const Strategies = {
|
export const Strategies = {
|
||||||
password: {
|
password: {
|
||||||
@@ -46,8 +45,7 @@ export const guardRoleSchema = s.strictObject({
|
|||||||
implicit_allow: s.boolean().optional(),
|
implicit_allow: s.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const authConfigSchema = $object(
|
export const authConfigSchema = s.strictObject(
|
||||||
"config_auth",
|
|
||||||
{
|
{
|
||||||
enabled: s.boolean({ default: false }),
|
enabled: s.boolean({ default: false }),
|
||||||
basepath: s.string({ default: "/api/auth" }),
|
basepath: s.string({ default: "/api/auth" }),
|
||||||
@@ -55,29 +53,20 @@ export const authConfigSchema = $object(
|
|||||||
allow_register: s.boolean({ default: true }).optional(),
|
allow_register: s.boolean({ default: true }).optional(),
|
||||||
jwt: jwtConfig,
|
jwt: jwtConfig,
|
||||||
cookie: cookieConfig,
|
cookie: cookieConfig,
|
||||||
strategies: $record(
|
strategies: s.record(strategiesSchema, {
|
||||||
"config_auth_strategies",
|
title: "Strategies",
|
||||||
strategiesSchema,
|
default: {
|
||||||
{
|
password: {
|
||||||
title: "Strategies",
|
type: "password",
|
||||||
default: {
|
enabled: true,
|
||||||
password: {
|
config: {
|
||||||
type: "password",
|
hashing: "sha256",
|
||||||
enabled: true,
|
|
||||||
config: {
|
|
||||||
hashing: "sha256",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
s.strictObject({
|
}),
|
||||||
type: s.string(),
|
|
||||||
enabled: s.boolean({ default: true }).optional(),
|
|
||||||
config: s.object({}),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
guard: guardConfigSchema.optional(),
|
guard: guardConfigSchema.optional(),
|
||||||
roles: $record("config_auth_roles", guardRoleSchema, { default: {} }).optional(),
|
roles: s.record(guardRoleSchema, { default: {} }).optional(),
|
||||||
},
|
},
|
||||||
{ title: "Authentication" },
|
{ title: "Authentication" },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import type { ServerEnv } from "modules/Controller";
|
|||||||
import { pick } from "lodash-es";
|
import { pick } from "lodash-es";
|
||||||
import { InvalidConditionsException } from "auth/errors";
|
import { InvalidConditionsException } from "auth/errors";
|
||||||
import { s, parse, secret, runtimeSupports, truncate, $console } from "bknd/utils";
|
import { s, parse, secret, runtimeSupports, truncate, $console } from "bknd/utils";
|
||||||
import { $object } from "modules/mcp";
|
|
||||||
import type { AuthStrategy } from "./strategies/Strategy";
|
import type { AuthStrategy } from "./strategies/Strategy";
|
||||||
|
|
||||||
type Input = any; // workaround
|
type Input = any; // workaround
|
||||||
@@ -43,7 +42,7 @@ export interface UserPool {
|
|||||||
|
|
||||||
const defaultCookieExpires = 60 * 60 * 24 * 7; // 1 week in seconds
|
const defaultCookieExpires = 60 * 60 * 24 * 7; // 1 week in seconds
|
||||||
export const cookieConfig = s
|
export const cookieConfig = s
|
||||||
.strictObject({
|
.object({
|
||||||
path: s.string({ default: "/" }),
|
path: s.string({ default: "/" }),
|
||||||
sameSite: s.string({ enum: ["strict", "lax", "none"], default: "lax" }),
|
sameSite: s.string({ enum: ["strict", "lax", "none"], default: "lax" }),
|
||||||
secure: s.boolean({ default: true }),
|
secure: s.boolean({ default: true }),
|
||||||
@@ -54,24 +53,27 @@ export const cookieConfig = s
|
|||||||
pathSuccess: s.string({ default: "/" }),
|
pathSuccess: s.string({ default: "/" }),
|
||||||
pathLoggedOut: s.string({ default: "/" }),
|
pathLoggedOut: s.string({ default: "/" }),
|
||||||
})
|
})
|
||||||
.partial();
|
.partial()
|
||||||
|
.strict();
|
||||||
|
|
||||||
// @todo: maybe add a config to not allow cookie/api tokens to be used interchangably?
|
// @todo: maybe add a config to not allow cookie/api tokens to be used interchangably?
|
||||||
// see auth.integration test for further details
|
// see auth.integration test for further details
|
||||||
|
|
||||||
export const jwtConfig = s.strictObject(
|
export const jwtConfig = s
|
||||||
{
|
.object(
|
||||||
secret: secret({ default: "" }),
|
{
|
||||||
alg: s.string({ enum: ["HS256", "HS384", "HS512"], default: "HS256" }).optional(),
|
// @todo: autogenerate a secret if not present. But it must be persisted from AppAuth
|
||||||
expires: s.number().optional(), // seconds
|
secret: secret({ default: "" }),
|
||||||
issuer: s.string().optional(),
|
alg: s.string({ enum: ["HS256", "HS384", "HS512"], default: "HS256" }).optional(),
|
||||||
fields: s.array(s.string(), { default: ["id", "email", "role"] }),
|
expires: s.number().optional(), // seconds
|
||||||
},
|
issuer: s.string().optional(),
|
||||||
{
|
fields: s.array(s.string(), { default: ["id", "email", "role"] }),
|
||||||
default: {},
|
},
|
||||||
},
|
{
|
||||||
);
|
default: {},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.strict();
|
||||||
export const authenticatorConfig = s.object({
|
export const authenticatorConfig = s.object({
|
||||||
jwt: jwtConfig,
|
jwt: jwtConfig,
|
||||||
cookie: cookieConfig,
|
cookie: cookieConfig,
|
||||||
@@ -278,9 +280,7 @@ export class Authenticator<
|
|||||||
}
|
}
|
||||||
|
|
||||||
return payload as any;
|
return payload as any;
|
||||||
} catch (e) {
|
} catch (e) {}
|
||||||
$console.debug("Authenticator jwt verify error", String(e));
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -378,29 +378,13 @@ export class Authenticator<
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @todo: don't extract user from token, but from the database or cache
|
// @todo: don't extract user from token, but from the database or cache
|
||||||
async resolveAuthFromRequest(c: Context | Request | Headers): Promise<SafeUser | undefined> {
|
async resolveAuthFromRequest(c: Context): Promise<SafeUser | undefined> {
|
||||||
let headers: Headers;
|
|
||||||
let is_context = false;
|
|
||||||
if (c instanceof Headers) {
|
|
||||||
headers = c;
|
|
||||||
} else if (c instanceof Request) {
|
|
||||||
headers = c.headers;
|
|
||||||
} else {
|
|
||||||
is_context = true;
|
|
||||||
try {
|
|
||||||
headers = c.req.raw.headers;
|
|
||||||
} catch (e) {
|
|
||||||
throw new Exception("Request/Headers/Context is required to resolve auth", 400);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let token: string | undefined;
|
let token: string | undefined;
|
||||||
if (headers.has("Authorization")) {
|
if (c.req.raw.headers.has("Authorization")) {
|
||||||
const bearerHeader = String(headers.get("Authorization"));
|
const bearerHeader = String(c.req.header("Authorization"));
|
||||||
token = bearerHeader.replace("Bearer ", "");
|
token = bearerHeader.replace("Bearer ", "");
|
||||||
} else {
|
} else {
|
||||||
const context = is_context ? (c as Context) : ({ req: { raw: { headers } } } as Context);
|
token = await this.getAuthCookie(c);
|
||||||
token = await this.getAuthCookie(context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import type { User } from "bknd";
|
import type { User } from "bknd";
|
||||||
import type { Authenticator } from "auth/authenticate/Authenticator";
|
import type { Authenticator } from "auth/authenticate/Authenticator";
|
||||||
import { InvalidCredentialsException } from "auth/errors";
|
import { InvalidCredentialsException } from "auth/errors";
|
||||||
import { hash, $console, s, parse, jsc } from "bknd/utils";
|
import { hash, $console } from "core/utils";
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { compare as bcryptCompare, genSalt as bcryptGenSalt, hash as bcryptHash } from "bcryptjs";
|
import { compare as bcryptCompare, genSalt as bcryptGenSalt, hash as bcryptHash } from "bcryptjs";
|
||||||
import { AuthStrategy } from "./Strategy";
|
import { AuthStrategy } from "./Strategy";
|
||||||
|
import { s, parse, jsc } from "bknd/utils";
|
||||||
|
|
||||||
const schema = s
|
const schema = s
|
||||||
.object({
|
.object({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Exception } from "core/errors";
|
import { Exception } from "core/errors";
|
||||||
import { $console, objectTransform } from "bknd/utils";
|
import { $console, objectTransform } from "core/utils";
|
||||||
import { Permission } from "core/security/Permission";
|
import { Permission } from "core/security/Permission";
|
||||||
import type { Context } from "hono";
|
import type { Context } from "hono";
|
||||||
import type { ServerEnv } from "modules/Controller";
|
import type { ServerEnv } from "modules/Controller";
|
||||||
|
|||||||
@@ -1,45 +1,15 @@
|
|||||||
import { getDefaultConfig } from "modules/ModuleManager";
|
import { getDefaultConfig } from "modules/ModuleManager";
|
||||||
import type { CliCommand } from "../types";
|
import type { CliCommand } from "../types";
|
||||||
import { makeAppFromEnv } from "cli/commands/run";
|
|
||||||
import { writeFile } from "node:fs/promises";
|
|
||||||
import c from "picocolors";
|
|
||||||
import { withConfigOptions } from "cli/utils/options";
|
|
||||||
import { $console } from "bknd/utils";
|
|
||||||
|
|
||||||
export const config: CliCommand = (program) => {
|
export const config: CliCommand = (program) => {
|
||||||
withConfigOptions(program.command("config"))
|
program
|
||||||
.description("get app config")
|
.command("config")
|
||||||
|
.description("get default config")
|
||||||
.option("--pretty", "pretty print")
|
.option("--pretty", "pretty print")
|
||||||
.option("--default", "use default config")
|
.action((options) => {
|
||||||
.option("--secrets", "include secrets in output")
|
const config = getDefaultConfig();
|
||||||
.option("--out <file>", "output file")
|
|
||||||
.action(async (options) => {
|
|
||||||
let config: any = {};
|
|
||||||
|
|
||||||
if (options.default) {
|
// biome-ignore lint/suspicious/noConsoleLog:
|
||||||
config = getDefaultConfig();
|
console.log(options.pretty ? JSON.stringify(config, null, 2) : JSON.stringify(config));
|
||||||
} else {
|
|
||||||
const app = await makeAppFromEnv(options);
|
|
||||||
const manager = app.modules;
|
|
||||||
|
|
||||||
if (options.secrets) {
|
|
||||||
$console.warn("Including secrets in output");
|
|
||||||
config = manager.toJSON(true);
|
|
||||||
} else {
|
|
||||||
config = manager.extractSecrets().configs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config = options.pretty ? JSON.stringify(config, null, 2) : JSON.stringify(config);
|
|
||||||
|
|
||||||
console.info("");
|
|
||||||
if (options.out) {
|
|
||||||
await writeFile(options.out, config);
|
|
||||||
console.info(`Config written to ${c.cyan(options.out)}`);
|
|
||||||
} else {
|
|
||||||
console.info(JSON.parse(config));
|
|
||||||
}
|
|
||||||
|
|
||||||
process.exit(0);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,5 +34,4 @@ async function action(options: { out?: string; clean?: boolean }) {
|
|||||||
|
|
||||||
// biome-ignore lint/suspicious/noConsoleLog:
|
// biome-ignore lint/suspicious/noConsoleLog:
|
||||||
console.log(c.green(`Assets copied to: ${c.bold(out)}`));
|
console.log(c.green(`Assets copied to: ${c.bold(out)}`));
|
||||||
process.exit(0);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as $p from "@clack/prompts";
|
import * as $p from "@clack/prompts";
|
||||||
import { overrideJson } from "cli/commands/create/npm";
|
import { overrideJson, overridePackageJson } from "cli/commands/create/npm";
|
||||||
import { typewriter } from "cli/utils/cli";
|
import { typewriter, wait } from "cli/utils/cli";
|
||||||
|
import { uuid } from "core/utils";
|
||||||
import c from "picocolors";
|
import c from "picocolors";
|
||||||
import type { Template, TemplateSetupCtx } from ".";
|
import type { Template, TemplateSetupCtx } from ".";
|
||||||
import { exec } from "cli/utils/sys";
|
import { exec } from "cli/utils/sys";
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user