mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 08:06:00 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 906f80b11e | |||
| 7a3691a1a7 |
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"bknd": {
|
|
||||||
"url": "http://localhost:28623/api/system/mcp"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,15 +12,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "22.x"
|
|
||||||
|
|
||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: oven-sh/setup-bun@v1
|
uses: oven-sh/setup-bun@v1
|
||||||
with:
|
with:
|
||||||
bun-version: "1.2.22"
|
bun-version: "1.2.14"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
working-directory: ./app
|
working-directory: ./app
|
||||||
|
|||||||
+1
-3
@@ -31,6 +31,4 @@ packages/media/.env
|
|||||||
.git_old
|
.git_old
|
||||||
docker/tmp
|
docker/tmp
|
||||||
.debug
|
.debug
|
||||||
.history
|
.history
|
||||||
.aider*
|
|
||||||
.vercel
|
|
||||||
@@ -1,50 +1,26 @@
|
|||||||
[](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>
|
||||||
|
|
||||||
bknd simplifies app development by providing a fully functional visual backend for database management, authentication, media and workflows. Being lightweight and built on Web Standards, it can be deployed nearly anywhere, including running inside your framework of choice. No more deploying multiple separate services!
|
bknd simplifies app development by providing a fully functional backend for database management, authentication, media and workflows. Being lightweight and built on Web Standards, it can be deployed nearly anywhere, including running inside your framework of choice. No more deploying multiple separate services!
|
||||||
|
|
||||||
It's designed to avoid vendor lock-in and architectural limitations. Built exclusively on [WinterTC Minimum Common Web Platform API](https://min-common-api.proposal.wintertc.org/) for universal compatibility, all functionality (data, auth, media, flows) is modular and opt-in, and infrastructure access is adapter-based with direct access to underlying drivers giving you full control without abstractions getting in your way.
|
|
||||||
|
|
||||||
* **Runtimes**: Node.js 22+, Bun 1.0+, Deno, Browser, Cloudflare Workers/Pages, Vercel, Netlify, AWS Lambda, etc.
|
|
||||||
* **Databases**:
|
|
||||||
* SQLite: LibSQL, Node SQLite, Bun SQLite, Cloudflare D1, Cloudflare Durable Objects SQLite, SQLocal
|
|
||||||
* Postgres: Vanilla Postgres, Supabase, Neon, Xata
|
|
||||||
* **Frameworks**: React, Next.js, React Router, Astro, Vite, Waku
|
|
||||||
* **Storage**: AWS S3, S3-compatible (Tigris, R2, Minio, etc.), Cloudflare R2 (binding), Cloudinary, Filesystem
|
|
||||||
* **Deployment**: Standalone, Docker, Cloudflare Workers, Vercel, Netlify, Deno Deploy, AWS Lambda, Valtown etc.
|
|
||||||
|
|
||||||
**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`).
|
|
||||||
>
|
|
||||||
> 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.
|
||||||
|
|
||||||
## Use Cases
|
|
||||||
|
|
||||||
bknd is a general purpose backend system that implements the primitives almost any backend needs. This way, you can use it for any backend use case, including but not limited to:
|
|
||||||
|
|
||||||
- **Content Management System (CMS)** as Wordpress alternative, hosted separately or embedded in your frontend
|
|
||||||
- **AI Agent Backends** for managing agent state with built-in data persistence, regardless where it is hosted. Optionally communicate over the integrated MCP server.
|
|
||||||
- **SaaS Products** with multi-tenant data isolation (RLS) and user management, with freedom to choose your own database and storage provider
|
|
||||||
- **Prototypes & MVPs** to validate ideas quickly without infrastructure overhead
|
|
||||||
- **API-First Applications** where you need a reliable, type-safe backend without vendor lock-in either with the integrated TypeScript SDK or REST API using OpenAPI
|
|
||||||
- **IoT & Embedded Devices** where minimal footprint matters
|
|
||||||
|
|
||||||
|
|
||||||
## 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.
|
||||||
|
|
||||||
@@ -62,7 +38,6 @@ Creating digital products always requires developing both the backend (the logic
|
|||||||
* **Media**: Effortlessly manage and serve all your media files.
|
* **Media**: Effortlessly manage and serve all your media files.
|
||||||
* **Flows**: Design and run workflows with seamless automation. (UI integration coming soon!)
|
* **Flows**: Design and run workflows with seamless automation. (UI integration coming soon!)
|
||||||
* 🌐 Built on Web Standards for maximum compatibility
|
* 🌐 Built on Web Standards for maximum compatibility
|
||||||
* 🛠️ MCP server, client and UI built-in to control your backend
|
|
||||||
* 🏃♂️ Multiple run modes
|
* 🏃♂️ Multiple run modes
|
||||||
* standalone using the CLI
|
* standalone using the CLI
|
||||||
* using a JavaScript runtime (Node, Bun, workerd)
|
* using a JavaScript runtime (Node, Bun, workerd)
|
||||||
|
|||||||
+13
-134
@@ -1,148 +1,27 @@
|
|||||||
import { afterEach, describe, test, expect, beforeAll, afterAll } from "bun:test";
|
import { afterAll, afterEach, describe, expect, test } from "bun:test";
|
||||||
import { App, createApp } from "core/test/utils";
|
import { App } from "../src";
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
import { Hono } from "hono";
|
|
||||||
import * as proto from "../src/data/prototype";
|
|
||||||
import { pick } from "lodash-es";
|
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
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 () => {
|
||||||
const app = new App(dummyConnection);
|
const app = new App(dummyConnection);
|
||||||
await app.build();
|
await app.build();
|
||||||
|
|
||||||
expect(await app.em.ping()).toBeTrue();
|
//expect(await app.data?.em.ping()).toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("plugins", async () => {
|
/*test.only("what", async () => {
|
||||||
const called: string[] = [];
|
const app = new App(dummyConnection, {
|
||||||
const app = createApp({
|
auth: {
|
||||||
config: {
|
enabled: true,
|
||||||
auth: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
plugins: [
|
|
||||||
(app) => {
|
|
||||||
expect(app).toBeDefined();
|
|
||||||
expect(app).toBeInstanceOf(App);
|
|
||||||
return {
|
|
||||||
name: "test",
|
|
||||||
schema: () => {
|
|
||||||
called.push("schema");
|
|
||||||
return proto.em(
|
|
||||||
{
|
|
||||||
posts: proto.entity("posts", {
|
|
||||||
title: proto.text(),
|
|
||||||
}),
|
|
||||||
comments: proto.entity("comments", {
|
|
||||||
content: proto.text(),
|
|
||||||
}),
|
|
||||||
users: proto.entity("users", {
|
|
||||||
email_verified: proto.boolean(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
(fn, s) => {
|
|
||||||
fn.relation(s.comments).manyToOne(s.posts);
|
|
||||||
fn.index(s.posts).on(["title"]);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onBoot: async () => {
|
|
||||||
called.push("onBoot");
|
|
||||||
},
|
|
||||||
beforeBuild: async () => {
|
|
||||||
called.push("beforeBuild");
|
|
||||||
},
|
|
||||||
onBuilt: async () => {
|
|
||||||
called.push("onBuilt");
|
|
||||||
},
|
|
||||||
onServerInit: async (server) => {
|
|
||||||
called.push("onServerInit");
|
|
||||||
expect(server).toBeDefined();
|
|
||||||
expect(server).toBeInstanceOf(Hono);
|
|
||||||
},
|
|
||||||
onFirstBoot: async () => {
|
|
||||||
called.push("onFirstBoot");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
await app.module.auth.build();
|
||||||
await app.build();
|
await app.module.data.build();
|
||||||
|
console.log(app.em.entities.map((e) => e.name));
|
||||||
expect(app.em.entities.map((e) => e.name)).toEqual(["users", "posts", "comments"]);
|
console.log(await app.em.schema().getDiff());
|
||||||
expect(app.em.indices.map((i) => i.name)).toEqual([
|
});*/
|
||||||
"idx_unique_users_email",
|
|
||||||
"idx_users_strategy",
|
|
||||||
"idx_users_strategy_value",
|
|
||||||
"idx_posts_title",
|
|
||||||
]);
|
|
||||||
expect(
|
|
||||||
app.em.relations.all.map((r) => pick(r.toJSON(), ["type", "source", "target"])),
|
|
||||||
).toEqual([
|
|
||||||
{
|
|
||||||
type: "n:1",
|
|
||||||
source: "comments",
|
|
||||||
target: "posts",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
expect(called).toEqual([
|
|
||||||
"onBoot",
|
|
||||||
"onServerInit",
|
|
||||||
"beforeBuild",
|
|
||||||
"onServerInit",
|
|
||||||
"schema",
|
|
||||||
"onFirstBoot",
|
|
||||||
"onBuilt",
|
|
||||||
]);
|
|
||||||
expect(app.plugins.size).toBe(1);
|
|
||||||
expect(Array.from(app.plugins.keys())).toEqual(["test"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.only("drivers", async () => {
|
|
||||||
const called: string[] = [];
|
|
||||||
const app = new App(dummyConnection, undefined, {
|
|
||||||
drivers: {
|
|
||||||
email: {
|
|
||||||
send: async (to, subject, body) => {
|
|
||||||
called.push("email.send");
|
|
||||||
return {
|
|
||||||
id: "",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cache: {
|
|
||||||
get: async (key) => {
|
|
||||||
called.push("cache.get");
|
|
||||||
return "";
|
|
||||||
},
|
|
||||||
set: async (key, value, ttl) => {
|
|
||||||
called.push("cache.set");
|
|
||||||
},
|
|
||||||
del: async (key) => {
|
|
||||||
called.push("cache.del");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await app.build();
|
|
||||||
|
|
||||||
expect(app.drivers.cache).toBeDefined();
|
|
||||||
expect(app.drivers.email).toBeDefined();
|
|
||||||
await app.drivers.email.send("", "", "");
|
|
||||||
await app.drivers.cache.get("");
|
|
||||||
await app.drivers.cache.set("", "", 0);
|
|
||||||
await app.drivers.cache.del("");
|
|
||||||
|
|
||||||
expect(called).toEqual(["email.send", "cache.get", "cache.set", "cache.del"]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
tmp/*
|
tmp/*
|
||||||
!tmp/.gitkeep
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
hello
|
|
||||||
@@ -1,57 +1,53 @@
|
|||||||
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";
|
||||||
import { bunTestRunner } from "adapter/bun/test";
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
import { omitKeys } from "core/utils";
|
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
beforeAll(disableConsoleLog);
|
||||||
afterAll(enableConsoleLog);
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
describe("adapter", () => {
|
describe("adapter", () => {
|
||||||
it("makes config", async () => {
|
it("makes config", () => {
|
||||||
expect(omitKeys(await adapter.makeConfig({}), ["connection"])).toEqual({});
|
expect(adapter.makeConfig({})).toEqual({});
|
||||||
expect(
|
expect(adapter.makeConfig({}, { env: { TEST: "test" } })).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(adapter.makeConfig({ app: (a) => a as any }, { env: { TEST: "test" } })).toEqual({
|
||||||
omitKeys(
|
env: { TEST: "test" },
|
||||||
await adapter.makeConfig(
|
} as any);
|
||||||
{ app: (a) => ({ config: { server: { cors: { origin: a.env.TEST } } } }) },
|
|
||||||
{ env: { TEST: "test" } },
|
|
||||||
),
|
|
||||||
["connection"],
|
|
||||||
),
|
|
||||||
).toEqual({
|
|
||||||
config: { server: { cors: { origin: "test" } } },
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows all properties in app function", async () => {
|
it("reuses apps correctly", async () => {
|
||||||
const called = mock(() => null);
|
const id = crypto.randomUUID();
|
||||||
const config = await adapter.makeConfig(
|
|
||||||
|
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, {
|
||||||
|
|||||||
@@ -6,16 +6,13 @@ describe("Api", async () => {
|
|||||||
it("should construct without options", () => {
|
it("should construct without options", () => {
|
||||||
const api = new Api();
|
const api = new Api();
|
||||||
expect(api.baseUrl).toBe("http://localhost");
|
expect(api.baseUrl).toBe("http://localhost");
|
||||||
|
expect(api.isAuthVerified()).toBe(false);
|
||||||
// verified is true, because no token, user, headers or request given
|
|
||||||
// therefore nothing to check, auth state is verified
|
|
||||||
expect(api.isAuthVerified()).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should ignore force verify if no claims given", () => {
|
it("should ignore force verify if no claims given", () => {
|
||||||
const api = new Api({ verified: true });
|
const api = new Api({ verified: true });
|
||||||
expect(api.baseUrl).toBe("http://localhost");
|
expect(api.baseUrl).toBe("http://localhost");
|
||||||
expect(api.isAuthVerified()).toBe(true);
|
expect(api.isAuthVerified()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should construct from request (token)", async () => {
|
it("should construct from request (token)", async () => {
|
||||||
@@ -45,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,12 +1,12 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||||
import { Guard } from "../../src/auth/authorize/Guard";
|
import { Guard } from "../../src/auth";
|
||||||
|
import { parse } from "../../src/core/utils";
|
||||||
import { DataApi } from "../../src/data/api/DataApi";
|
import { DataApi } from "../../src/data/api/DataApi";
|
||||||
import { DataController } from "../../src/data/api/DataController";
|
import { DataController } from "../../src/data/api/DataController";
|
||||||
import { dataConfigSchema } from "../../src/data/data-schema";
|
import { dataConfigSchema } from "../../src/data/data-schema";
|
||||||
import * as proto from "../../src/data/prototype";
|
import * as proto from "../../src/data/prototype";
|
||||||
import { schemaToEm } from "../helper";
|
import { schemaToEm } from "../helper";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
import { parse } from "core/utils/schema";
|
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
beforeAll(disableConsoleLog);
|
||||||
afterAll(enableConsoleLog);
|
afterAll(enableConsoleLog);
|
||||||
@@ -153,7 +153,7 @@ describe("DataApi", () => {
|
|||||||
const oneBy = api.readOneBy("posts", { where: { title: "baz" }, select: ["title"] });
|
const oneBy = api.readOneBy("posts", { where: { title: "baz" }, select: ["title"] });
|
||||||
const oneByRes = await oneBy;
|
const oneByRes = await oneBy;
|
||||||
expect(oneByRes.data).toEqual({ title: "baz" } as any);
|
expect(oneByRes.data).toEqual({ title: "baz" } as any);
|
||||||
expect(oneByRes.body.meta.items).toEqual(1);
|
expect(oneByRes.body.meta.count).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("exists/count", async () => {
|
it("exists/count", async () => {
|
||||||
@@ -202,7 +202,7 @@ describe("DataApi", () => {
|
|||||||
{
|
{
|
||||||
// create many
|
// create many
|
||||||
const res = await api.createMany("posts", payload);
|
const res = await api.createMany("posts", payload);
|
||||||
expect(res.data?.length).toEqual(4);
|
expect(res.data.length).toEqual(4);
|
||||||
expect(res.ok).toBeTrue();
|
expect(res.ok).toBeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,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 "../../src/App";
|
||||||
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,8 +20,6 @@ describe("App", () => {
|
|||||||
"guard",
|
"guard",
|
||||||
"flags",
|
"flags",
|
||||||
"logger",
|
"logger",
|
||||||
"mcp",
|
|
||||||
"helper",
|
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -43,7 +27,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 +134,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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
import { AppServer, serverConfigSchema } from "modules/server/AppServer";
|
|
||||||
import { describe, test, expect } from "bun:test";
|
|
||||||
|
|
||||||
describe("AppServer", () => {
|
|
||||||
test("config", () => {
|
|
||||||
{
|
|
||||||
const server = new AppServer();
|
|
||||||
expect(server).toBeDefined();
|
|
||||||
expect(server.config).toEqual({
|
|
||||||
cors: {
|
|
||||||
allow_credentials: true,
|
|
||||||
origin: "*",
|
|
||||||
allow_methods: ["GET", "POST", "PATCH", "PUT", "DELETE"],
|
|
||||||
allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"],
|
|
||||||
},
|
|
||||||
mcp: {
|
|
||||||
enabled: false,
|
|
||||||
path: "/api/system/mcp",
|
|
||||||
logLevel: "warning",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const server = new AppServer({
|
|
||||||
cors: {
|
|
||||||
origin: "https",
|
|
||||||
allow_methods: ["GET", "POST"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(server).toBeDefined();
|
|
||||||
expect(server.config).toEqual({
|
|
||||||
cors: {
|
|
||||||
allow_credentials: true,
|
|
||||||
origin: "https",
|
|
||||||
allow_methods: ["GET", "POST"],
|
|
||||||
allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"],
|
|
||||||
},
|
|
||||||
mcp: {
|
|
||||||
enabled: false,
|
|
||||||
path: "/api/system/mcp",
|
|
||||||
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,232 +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.map((p) => p.permission)).toEqual([
|
|
||||||
"read",
|
|
||||||
"write",
|
|
||||||
]);
|
|
||||||
|
|
||||||
// update role
|
|
||||||
await tool(server, "config_auth_roles_update", {
|
|
||||||
key: "guest",
|
|
||||||
value: {
|
|
||||||
permissions: ["read"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(app.toJSON().auth.roles?.guest?.permissions?.map((p) => p.permission)).toEqual([
|
|
||||||
"read",
|
|
||||||
]);
|
|
||||||
|
|
||||||
// get role
|
|
||||||
const getGuestRole = await tool(server, "config_auth_roles_get", {
|
|
||||||
key: "guest",
|
|
||||||
});
|
|
||||||
expect(getGuestRole.value.permissions.map((p) => p.permission)).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,7 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { registries } from "../../src";
|
import { createApp, registries } from "../../src";
|
||||||
import { createApp } from "core/test/utils";
|
|
||||||
import * as proto from "../../src/data/prototype";
|
import * as proto from "../../src/data/prototype";
|
||||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
describe("repros", async () => {
|
describe("repros", async () => {
|
||||||
/**
|
/**
|
||||||
@@ -92,7 +87,7 @@ describe("repros", async () => {
|
|||||||
fns.relation(schema.product_likes).manyToOne(schema.users);
|
fns.relation(schema.product_likes).manyToOne(schema.users);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const app = createApp({ config: { data: schema.toJSON() } });
|
const app = createApp({ initialConfig: { data: schema.toJSON() } });
|
||||||
await app.build();
|
await app.build();
|
||||||
|
|
||||||
const info = (await (await app.server.request("/api/data/info/products")).json()) as any;
|
const info = (await (await app.server.request("/api/data/info/products")).json()) as any;
|
||||||
|
|||||||
@@ -1,41 +1,46 @@
|
|||||||
import { Authenticator } from "auth/authenticate/Authenticator";
|
|
||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { Authenticator, type User, type UserPool } from "../../src/auth";
|
||||||
|
import { cookieConfig } from "../../src/auth/authenticate/Authenticator";
|
||||||
|
import { PasswordStrategy } from "../../src/auth/authenticate/strategies/PasswordStrategy";
|
||||||
|
import * as hash from "../../src/auth/utils/hash";
|
||||||
|
import { Default, parse } from "../../src/core/utils";
|
||||||
|
|
||||||
|
/*class MemoryUserPool implements UserPool {
|
||||||
|
constructor(private users: User[] = []) {}
|
||||||
|
|
||||||
|
async findBy(prop: "id" | "email" | "username", value: string | number) {
|
||||||
|
return this.users.find((user) => user[prop] === value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(user: Pick<User, "email" | "password">) {
|
||||||
|
const id = this.users.length + 1;
|
||||||
|
const newUser = { ...user, id, username: user.email };
|
||||||
|
this.users.push(newUser);
|
||||||
|
return newUser;
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
describe("Authenticator", async () => {
|
describe("Authenticator", async () => {
|
||||||
test("should return auth cookie headers", async () => {
|
test("cookie options", async () => {
|
||||||
const auth = new Authenticator({}, null as any, {
|
console.log("parsed", parse(cookieConfig, undefined));
|
||||||
jwt: {
|
console.log(Default(cookieConfig, {}));
|
||||||
secret: "secret",
|
|
||||||
fields: [],
|
|
||||||
},
|
|
||||||
cookie: {
|
|
||||||
sameSite: "strict",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const headers = await auth.getAuthCookieHeader("token");
|
|
||||||
const cookie = headers.get("Set-Cookie");
|
|
||||||
expect(cookie).toStartWith("auth=");
|
|
||||||
expect(cookie).toEndWith("HttpOnly; Secure; SameSite=Strict");
|
|
||||||
|
|
||||||
// now expect it to be removed
|
|
||||||
const headers2 = await auth.removeAuthCookieHeader(headers);
|
|
||||||
const cookie2 = headers2.get("Set-Cookie");
|
|
||||||
expect(cookie2).toStartWith("auth=; Max-Age=0; Path=/; Expires=");
|
|
||||||
expect(cookie2).toEndWith("HttpOnly; Secure; SameSite=Strict");
|
|
||||||
});
|
});
|
||||||
|
/*const userpool = new MemoryUserPool([
|
||||||
|
{ id: 1, email: "d", username: "test", password: await hash.sha256("test") },
|
||||||
|
]);
|
||||||
|
|
||||||
test("should return auth cookie string", async () => {
|
test("sha256 login", async () => {
|
||||||
const auth = new Authenticator({}, null as any, {
|
const auth = new Authenticator(userpool, {
|
||||||
jwt: {
|
password: new PasswordStrategy({
|
||||||
secret: "secret",
|
hashing: "sha256",
|
||||||
fields: [],
|
}),
|
||||||
},
|
|
||||||
cookie: {
|
|
||||||
sameSite: "strict",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
const cookie = await auth.unsafeGetAuthCookie("token");
|
|
||||||
expect(cookie).toStartWith("auth=");
|
const { token } = await auth.login("password", { email: "d", password: "test" });
|
||||||
expect(cookie).toEndWith("HttpOnly; Secure; SameSite=Strict");
|
expect(token).toBeDefined();
|
||||||
});
|
|
||||||
|
const { iat, ...decoded } = decodeJwt<any>(token);
|
||||||
|
expect(decoded).toEqual({ id: 1, email: "d", username: "test" });
|
||||||
|
expect(await auth.verify(token)).toBe(true);
|
||||||
|
});*/
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,31 +1,9 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Guard, type GuardConfig } from "auth/authorize/Guard";
|
import { Guard } from "../../../src/auth";
|
||||||
import { Permission } from "auth/authorize/Permission";
|
|
||||||
import { Role, type RoleSchema } from "auth/authorize/Role";
|
|
||||||
import { objectTransform, s } from "bknd/utils";
|
|
||||||
|
|
||||||
function createGuard(
|
|
||||||
permissionNames: string[],
|
|
||||||
roles?: Record<string, Omit<RoleSchema, "name">>,
|
|
||||||
config?: GuardConfig,
|
|
||||||
) {
|
|
||||||
const _roles = roles
|
|
||||||
? objectTransform(roles, ({ permissions = [], is_default, implicit_allow }, name) => {
|
|
||||||
return Role.create(name, { permissions, is_default, implicit_allow });
|
|
||||||
})
|
|
||||||
: {};
|
|
||||||
const _permissions = permissionNames.map((name) => new Permission(name));
|
|
||||||
return new Guard(_permissions, Object.values(_roles), config);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("authorize", () => {
|
describe("authorize", () => {
|
||||||
const read = new Permission("read", {
|
|
||||||
filterable: true,
|
|
||||||
});
|
|
||||||
const write = new Permission("write");
|
|
||||||
|
|
||||||
test("basic", async () => {
|
test("basic", async () => {
|
||||||
const guard = createGuard(
|
const guard = Guard.create(
|
||||||
["read", "write"],
|
["read", "write"],
|
||||||
{
|
{
|
||||||
admin: {
|
admin: {
|
||||||
@@ -38,14 +16,14 @@ describe("authorize", () => {
|
|||||||
role: "admin",
|
role: "admin",
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(guard.granted(read, user)).toBeUndefined();
|
expect(guard.granted("read", user)).toBe(true);
|
||||||
expect(guard.granted(write, user)).toBeUndefined();
|
expect(guard.granted("write", user)).toBe(true);
|
||||||
|
|
||||||
expect(() => guard.granted(new Permission("something"), {})).toThrow();
|
expect(() => guard.granted("something")).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("with default", async () => {
|
test("with default", async () => {
|
||||||
const guard = createGuard(
|
const guard = Guard.create(
|
||||||
["read", "write"],
|
["read", "write"],
|
||||||
{
|
{
|
||||||
admin: {
|
admin: {
|
||||||
@@ -59,26 +37,26 @@ describe("authorize", () => {
|
|||||||
{ enabled: true },
|
{ enabled: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(guard.granted(read, {})).toBeUndefined();
|
expect(guard.granted("read")).toBe(true);
|
||||||
expect(() => guard.granted(write, {})).toThrow();
|
expect(guard.granted("write")).toBe(false);
|
||||||
|
|
||||||
const user = {
|
const user = {
|
||||||
role: "admin",
|
role: "admin",
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(guard.granted(read, user)).toBeUndefined();
|
expect(guard.granted("read", user)).toBe(true);
|
||||||
expect(guard.granted(write, user)).toBeUndefined();
|
expect(guard.granted("write", user)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("guard implicit allow", async () => {
|
test("guard implicit allow", async () => {
|
||||||
const guard = createGuard([], {}, { enabled: false });
|
const guard = Guard.create([], {}, { enabled: false });
|
||||||
|
|
||||||
expect(guard.granted(read, {})).toBeUndefined();
|
expect(guard.granted("read")).toBe(true);
|
||||||
expect(guard.granted(write, {})).toBeUndefined();
|
expect(guard.granted("write")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("role implicit allow", async () => {
|
test("role implicit allow", async () => {
|
||||||
const guard = createGuard(["read", "write"], {
|
const guard = Guard.create(["read", "write"], {
|
||||||
admin: {
|
admin: {
|
||||||
implicit_allow: true,
|
implicit_allow: true,
|
||||||
},
|
},
|
||||||
@@ -88,12 +66,12 @@ describe("authorize", () => {
|
|||||||
role: "admin",
|
role: "admin",
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(guard.granted(read, user)).toBeUndefined();
|
expect(guard.granted("read", user)).toBe(true);
|
||||||
expect(guard.granted(write, user)).toBeUndefined();
|
expect(guard.granted("write", user)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("guard with guest role implicit allow", async () => {
|
test("guard with guest role implicit allow", async () => {
|
||||||
const guard = createGuard(["read", "write"], {
|
const guard = Guard.create(["read", "write"], {
|
||||||
guest: {
|
guest: {
|
||||||
implicit_allow: true,
|
implicit_allow: true,
|
||||||
is_default: true,
|
is_default: true,
|
||||||
@@ -101,143 +79,7 @@ describe("authorize", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(guard.getUserRole()?.name).toBe("guest");
|
expect(guard.getUserRole()?.name).toBe("guest");
|
||||||
expect(guard.granted(read, {})).toBeUndefined();
|
expect(guard.granted("read")).toBe(true);
|
||||||
expect(guard.granted(write, {})).toBeUndefined();
|
expect(guard.granted("write")).toBe(true);
|
||||||
});
|
|
||||||
|
|
||||||
describe("cases", () => {
|
|
||||||
test("guest none, member deny if user.enabled is false", () => {
|
|
||||||
const guard = createGuard(
|
|
||||||
["read"],
|
|
||||||
{
|
|
||||||
guest: {
|
|
||||||
is_default: true,
|
|
||||||
},
|
|
||||||
member: {
|
|
||||||
permissions: [
|
|
||||||
{
|
|
||||||
permission: "read",
|
|
||||||
policies: [
|
|
||||||
{
|
|
||||||
condition: {},
|
|
||||||
effect: "filter",
|
|
||||||
filter: {
|
|
||||||
type: "member",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
condition: {
|
|
||||||
"user.enabled": false,
|
|
||||||
},
|
|
||||||
effect: "deny",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ enabled: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(() => guard.granted(read, { role: "guest" })).toThrow();
|
|
||||||
|
|
||||||
// member is allowed, because default role permission effect is allow
|
|
||||||
// and no deny policy is met
|
|
||||||
expect(guard.granted(read, { role: "member" })).toBeUndefined();
|
|
||||||
|
|
||||||
// member is allowed, because deny policy is not met
|
|
||||||
expect(guard.granted(read, { role: "member", enabled: true })).toBeUndefined();
|
|
||||||
|
|
||||||
// member is denied, because deny policy is met
|
|
||||||
expect(() => guard.granted(read, { role: "member", enabled: false })).toThrow();
|
|
||||||
|
|
||||||
// get the filter for member role
|
|
||||||
expect(guard.filters(read, { role: "member" }).filter).toEqual({
|
|
||||||
type: "member",
|
|
||||||
});
|
|
||||||
|
|
||||||
// get filter for guest
|
|
||||||
expect(guard.filters(read, {}).filter).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("guest should only read posts that are public", () => {
|
|
||||||
const read = new Permission(
|
|
||||||
"read",
|
|
||||||
{
|
|
||||||
// make this permission filterable
|
|
||||||
// without this, `filter` policies have no effect
|
|
||||||
filterable: true,
|
|
||||||
},
|
|
||||||
// expect the context to match this schema
|
|
||||||
// otherwise exit with 500 to ensure proper policy checking
|
|
||||||
s.object({
|
|
||||||
entity: s.string(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const guard = createGuard(
|
|
||||||
["read"],
|
|
||||||
{
|
|
||||||
guest: {
|
|
||||||
// this permission is applied if no (or invalid) role is provided
|
|
||||||
is_default: true,
|
|
||||||
permissions: [
|
|
||||||
{
|
|
||||||
permission: "read",
|
|
||||||
// effect deny means only having this permission, doesn't guarantee access
|
|
||||||
effect: "deny",
|
|
||||||
policies: [
|
|
||||||
{
|
|
||||||
// only if this condition is met
|
|
||||||
condition: {
|
|
||||||
entity: {
|
|
||||||
$in: ["posts"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// the effect is allow
|
|
||||||
effect: "allow",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
condition: {
|
|
||||||
entity: "posts",
|
|
||||||
},
|
|
||||||
effect: "filter",
|
|
||||||
filter: {
|
|
||||||
public: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
// members should be allowed to read all
|
|
||||||
member: {
|
|
||||||
permissions: [
|
|
||||||
{
|
|
||||||
permission: "read",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ enabled: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
// guest can only read posts
|
|
||||||
expect(guard.granted(read, {}, { entity: "posts" })).toBeUndefined();
|
|
||||||
expect(() => guard.granted(read, {}, { entity: "users" })).toThrow();
|
|
||||||
|
|
||||||
// and guests can only read public posts
|
|
||||||
expect(guard.filters(read, {}, { entity: "posts" }).filter).toEqual({
|
|
||||||
public: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// member can read posts and users
|
|
||||||
expect(guard.granted(read, { role: "member" }, { entity: "posts" })).toBeUndefined();
|
|
||||||
expect(guard.granted(read, { role: "member" }, { entity: "users" })).toBeUndefined();
|
|
||||||
|
|
||||||
// member should not have a filter
|
|
||||||
expect(
|
|
||||||
guard.filters(read, { role: "member" }, { entity: "posts" }).filter,
|
|
||||||
).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,327 +0,0 @@
|
|||||||
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
|
|
||||||
import { createApp } from "core/test/utils";
|
|
||||||
import type { CreateAppConfig } from "App";
|
|
||||||
import * as proto from "data/prototype";
|
|
||||||
import { mergeObject } from "core/utils/objects";
|
|
||||||
import type { App, DB } from "bknd";
|
|
||||||
import type { CreateUserPayload } from "auth/AppAuth";
|
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
afterAll(() => enableConsoleLog());
|
|
||||||
|
|
||||||
async function makeApp(config: Partial<CreateAppConfig["config"]> = {}) {
|
|
||||||
const app = createApp({
|
|
||||||
config: mergeObject(
|
|
||||||
{
|
|
||||||
data: proto
|
|
||||||
.em(
|
|
||||||
{
|
|
||||||
users: proto.systemEntity("users", {}),
|
|
||||||
posts: proto.entity("posts", {
|
|
||||||
title: proto.text(),
|
|
||||||
content: proto.text(),
|
|
||||||
}),
|
|
||||||
comments: proto.entity("comments", {
|
|
||||||
content: proto.text(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
({ relation }, { users, posts, comments }) => {
|
|
||||||
relation(posts).manyToOne(users);
|
|
||||||
relation(comments).manyToOne(posts);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.toJSON(),
|
|
||||||
auth: {
|
|
||||||
enabled: true,
|
|
||||||
jwt: {
|
|
||||||
secret: "secret",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
config,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
await app.build();
|
|
||||||
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createUsers(app: App, users: CreateUserPayload[]) {
|
|
||||||
return Promise.all(
|
|
||||||
users.map(async (user) => {
|
|
||||||
return await app.createUser(user);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadFixtures(app: App, fixtures: Record<string, any[]> = {}) {
|
|
||||||
const results = {} as any;
|
|
||||||
for (const [entity, data] of Object.entries(fixtures)) {
|
|
||||||
results[entity] = await app.em
|
|
||||||
.mutator(entity as any)
|
|
||||||
.insertMany(data)
|
|
||||||
.then((result) => result.data);
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("data permissions", async () => {
|
|
||||||
const app = await makeApp({
|
|
||||||
server: {
|
|
||||||
mcp: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
auth: {
|
|
||||||
guard: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
roles: {
|
|
||||||
guest: {
|
|
||||||
is_default: true,
|
|
||||||
permissions: [
|
|
||||||
{
|
|
||||||
permission: "system.access.api",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
permission: "data.entity.read",
|
|
||||||
policies: [
|
|
||||||
{
|
|
||||||
condition: {
|
|
||||||
entity: "posts",
|
|
||||||
},
|
|
||||||
effect: "filter",
|
|
||||||
filter: {
|
|
||||||
users_id: { $isnull: 1 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
permission: "data.entity.create",
|
|
||||||
policies: [
|
|
||||||
{
|
|
||||||
condition: {
|
|
||||||
entity: "posts",
|
|
||||||
},
|
|
||||||
effect: "filter",
|
|
||||||
filter: {
|
|
||||||
users_id: { $isnull: 1 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
permission: "data.entity.update",
|
|
||||||
policies: [
|
|
||||||
{
|
|
||||||
condition: {
|
|
||||||
entity: "posts",
|
|
||||||
},
|
|
||||||
effect: "filter",
|
|
||||||
filter: {
|
|
||||||
users_id: { $isnull: 1 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
permission: "data.entity.delete",
|
|
||||||
policies: [
|
|
||||||
{
|
|
||||||
condition: { entity: "posts" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
condition: { entity: "posts" },
|
|
||||||
effect: "filter",
|
|
||||||
filter: {
|
|
||||||
users_id: { $isnull: 1 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const users = [
|
|
||||||
{ email: "foo@example.com", password: "password" },
|
|
||||||
{ email: "bar@example.com", password: "password" },
|
|
||||||
];
|
|
||||||
const fixtures = {
|
|
||||||
posts: [
|
|
||||||
{ content: "post 1", users_id: 1 },
|
|
||||||
{ content: "post 2", users_id: 2 },
|
|
||||||
{ content: "post 3", users_id: null },
|
|
||||||
],
|
|
||||||
comments: [
|
|
||||||
{ content: "comment 1", posts_id: 1 },
|
|
||||||
{ content: "comment 2", posts_id: 2 },
|
|
||||||
{ content: "comment 3", posts_id: 3 },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
await createUsers(app, users);
|
|
||||||
const results = await loadFixtures(app, fixtures);
|
|
||||||
|
|
||||||
describe("http", async () => {
|
|
||||||
it("read many", async () => {
|
|
||||||
// many only includes posts with users_id is null
|
|
||||||
const res = await app.server.request("/api/data/entity/posts");
|
|
||||||
const data = await res.json().then((r: any) => r.data);
|
|
||||||
expect(data).toEqual([results.posts[2]]);
|
|
||||||
|
|
||||||
// same with /query
|
|
||||||
{
|
|
||||||
const res = await app.server.request("/api/data/entity/posts/query", {
|
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
const data = await res.json().then((r: any) => r.data);
|
|
||||||
expect(data).toEqual([results.posts[2]]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("read one", async () => {
|
|
||||||
// one only includes posts with users_id is null
|
|
||||||
{
|
|
||||||
const res = await app.server.request("/api/data/entity/posts/1");
|
|
||||||
const data = await res.json().then((r: any) => r.data);
|
|
||||||
expect(res.status).toBe(404);
|
|
||||||
expect(data).toBeUndefined();
|
|
||||||
}
|
|
||||||
|
|
||||||
// read one by allowed id
|
|
||||||
{
|
|
||||||
const res = await app.server.request("/api/data/entity/posts/3");
|
|
||||||
const data = await res.json().then((r: any) => r.data);
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
expect(data).toEqual(results.posts[2]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("read many by reference", async () => {
|
|
||||||
const res = await app.server.request("/api/data/entity/posts/1/comments");
|
|
||||||
const data = await res.json().then((r: any) => r.data);
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
expect(data).toEqual(results.comments.filter((c: any) => c.posts_id === 1));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("mutation create one", async () => {
|
|
||||||
// not allowed
|
|
||||||
{
|
|
||||||
const res = await app.server.request("/api/data/entity/posts", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ content: "post 4" }),
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(403);
|
|
||||||
}
|
|
||||||
// allowed
|
|
||||||
{
|
|
||||||
const res = await app.server.request("/api/data/entity/posts", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ content: "post 4", users_id: null }),
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(201);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("mutation update one", async () => {
|
|
||||||
// update one: not allowed
|
|
||||||
const res = await app.server.request("/api/data/entity/posts/1", {
|
|
||||||
method: "PATCH",
|
|
||||||
body: JSON.stringify({ content: "post 4" }),
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(403);
|
|
||||||
|
|
||||||
{
|
|
||||||
// update one: allowed
|
|
||||||
const res = await app.server.request("/api/data/entity/posts/3", {
|
|
||||||
method: "PATCH",
|
|
||||||
body: JSON.stringify({ content: "post 3 (updated)" }),
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
expect(await res.json().then((r: any) => r.data.content)).toBe("post 3 (updated)");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("mutation update many", async () => {
|
|
||||||
// update many: not allowed
|
|
||||||
const res = await app.server.request("/api/data/entity/posts", {
|
|
||||||
method: "PATCH",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
update: { content: "post 4" },
|
|
||||||
where: { users_id: { $isnull: 0 } },
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(200); // because filtered
|
|
||||||
const _data = await res.json().then((r: any) => r.data.map((p: any) => p.users_id));
|
|
||||||
expect(_data.every((u: any) => u === null)).toBe(true);
|
|
||||||
|
|
||||||
// verify
|
|
||||||
const data = await app.em
|
|
||||||
.repo("posts")
|
|
||||||
.findMany({ select: ["content", "users_id"] })
|
|
||||||
.then((r) => r.data);
|
|
||||||
|
|
||||||
// expect non null users_id to not have content "post 4"
|
|
||||||
expect(
|
|
||||||
data.filter((p: any) => p.users_id !== null).every((p: any) => p.content !== "post 4"),
|
|
||||||
).toBe(true);
|
|
||||||
// expect null users_id to have content "post 4"
|
|
||||||
expect(
|
|
||||||
data.filter((p: any) => p.users_id === null).every((p: any) => p.content === "post 4"),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
const count = async () => {
|
|
||||||
const {
|
|
||||||
data: { count: _count },
|
|
||||||
} = await app.em.repo("posts").count();
|
|
||||||
return _count;
|
|
||||||
};
|
|
||||||
it("mutation delete one", async () => {
|
|
||||||
const initial = await count();
|
|
||||||
|
|
||||||
// delete one: not allowed
|
|
||||||
const res = await app.server.request("/api/data/entity/posts/1", {
|
|
||||||
method: "DELETE",
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(403);
|
|
||||||
expect(await count()).toBe(initial);
|
|
||||||
|
|
||||||
{
|
|
||||||
// delete one: allowed
|
|
||||||
const res = await app.server.request("/api/data/entity/posts/3", {
|
|
||||||
method: "DELETE",
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
expect(await count()).toBe(initial - 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("mutation delete many", async () => {
|
|
||||||
// delete many: not allowed
|
|
||||||
const res = await app.server.request("/api/data/entity/posts", {
|
|
||||||
method: "DELETE",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
where: {},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
|
|
||||||
// only deleted posts with users_id is null
|
|
||||||
const remaining = await app.em
|
|
||||||
.repo("posts")
|
|
||||||
.findMany()
|
|
||||||
.then((r) => r.data);
|
|
||||||
expect(remaining.every((p: any) => p.users_id !== null)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { describe, it, expect } from "bun:test";
|
|
||||||
import { SystemController } from "modules/server/SystemController";
|
|
||||||
import { createApp } from "core/test/utils";
|
|
||||||
import type { CreateAppConfig } from "App";
|
|
||||||
import { getPermissionRoutes } from "auth/middlewares/permission.middleware";
|
|
||||||
|
|
||||||
async function makeApp(config: Partial<CreateAppConfig> = {}) {
|
|
||||||
const app = createApp(config);
|
|
||||||
await app.build();
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe.skip("SystemController", () => {
|
|
||||||
it("...", async () => {
|
|
||||||
const app = await makeApp();
|
|
||||||
const controller = new SystemController(app);
|
|
||||||
const hono = controller.getController();
|
|
||||||
console.log(getPermissionRoutes(hono));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,543 +0,0 @@
|
|||||||
import { describe, it, expect } from "bun:test";
|
|
||||||
import { s } from "bknd/utils";
|
|
||||||
import { Permission } from "auth/authorize/Permission";
|
|
||||||
import { Policy } from "auth/authorize/Policy";
|
|
||||||
import { Hono } from "hono";
|
|
||||||
import { getPermissionRoutes, permission } from "auth/middlewares/permission.middleware";
|
|
||||||
import { auth } from "auth/middlewares/auth.middleware";
|
|
||||||
import { Guard, mergeFilters, type GuardConfig } from "auth/authorize/Guard";
|
|
||||||
import { Role, RolePermission } from "auth/authorize/Role";
|
|
||||||
import { Exception } from "bknd";
|
|
||||||
import { convert } from "core/object/query/object-query";
|
|
||||||
|
|
||||||
describe("Permission", () => {
|
|
||||||
it("works with minimal schema", () => {
|
|
||||||
expect(() => new Permission("test")).not.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("parses context", () => {
|
|
||||||
const p = new Permission(
|
|
||||||
"test3",
|
|
||||||
{
|
|
||||||
filterable: true,
|
|
||||||
},
|
|
||||||
s.object({
|
|
||||||
a: s.string(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
expect(() => p.parseContext({ a: [] })).toThrow();
|
|
||||||
expect(p.parseContext({ a: "test" })).toEqual({ a: "test" });
|
|
||||||
// @ts-expect-error
|
|
||||||
expect(p.parseContext({ a: 1 })).toEqual({ a: "1" });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Policy", () => {
|
|
||||||
it("works with minimal schema", () => {
|
|
||||||
expect(() => new Policy().toJSON()).not.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("checks condition", () => {
|
|
||||||
const p = new Policy({
|
|
||||||
condition: {
|
|
||||||
a: 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(p.meetsCondition({ a: 1 })).toBe(true);
|
|
||||||
expect(p.meetsCondition({ a: 2 })).toBe(false);
|
|
||||||
expect(p.meetsCondition({ a: 1, b: 1 })).toBe(true);
|
|
||||||
expect(p.meetsCondition({})).toBe(false);
|
|
||||||
|
|
||||||
const p2 = new Policy({
|
|
||||||
condition: {
|
|
||||||
a: { $gt: 1 },
|
|
||||||
$or: {
|
|
||||||
b: { $lt: 2 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(p2.meetsCondition({ a: 2 })).toBe(true);
|
|
||||||
expect(p2.meetsCondition({ a: 1 })).toBe(false);
|
|
||||||
expect(p2.meetsCondition({ a: 1, b: 1 })).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("filters", () => {
|
|
||||||
const p = new Policy({
|
|
||||||
filter: {
|
|
||||||
age: { $gt: 18 },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const subjects = [{ age: 19 }, { age: 17 }, { age: 12 }];
|
|
||||||
|
|
||||||
expect(p.getFiltered(subjects)).toEqual([{ age: 19 }]);
|
|
||||||
|
|
||||||
expect(p.meetsFilter({ age: 19 })).toBe(true);
|
|
||||||
expect(p.meetsFilter({ age: 17 })).toBe(false);
|
|
||||||
expect(p.meetsFilter({ age: 12 })).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("replaces placeholders", () => {
|
|
||||||
const p = new Policy({
|
|
||||||
condition: {
|
|
||||||
a: "@auth.username",
|
|
||||||
},
|
|
||||||
filter: {
|
|
||||||
a: "@auth.username",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const vars = { auth: { username: "test" } };
|
|
||||||
|
|
||||||
expect(p.meetsCondition({ a: "test" }, vars)).toBe(true);
|
|
||||||
expect(p.meetsCondition({ a: "test2" }, vars)).toBe(false);
|
|
||||||
expect(p.meetsCondition({ a: "test2" })).toBe(false);
|
|
||||||
expect(p.meetsFilter({ a: "test" }, vars)).toBe(true);
|
|
||||||
expect(p.meetsFilter({ a: "test2" }, vars)).toBe(false);
|
|
||||||
expect(p.meetsFilter({ a: "test2" })).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Guard", () => {
|
|
||||||
it("collects filters", () => {
|
|
||||||
const p = new Permission(
|
|
||||||
"test",
|
|
||||||
{
|
|
||||||
filterable: true,
|
|
||||||
},
|
|
||||||
s.object({
|
|
||||||
a: s.number(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const r = new Role("test", [
|
|
||||||
new RolePermission(p, [
|
|
||||||
new Policy({
|
|
||||||
condition: { a: { $eq: 1 } },
|
|
||||||
filter: { foo: "bar" },
|
|
||||||
effect: "filter",
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
const guard = new Guard([p], [r], {
|
|
||||||
enabled: true,
|
|
||||||
});
|
|
||||||
expect(guard.filters(p, { role: r.name }, { a: 1 }).filter).toEqual({ foo: "bar" });
|
|
||||||
expect(guard.filters(p, { role: r.name }, { a: 2 }).filter).toBeUndefined();
|
|
||||||
// if no user context given, filter cannot be applied
|
|
||||||
expect(guard.filters(p, {}, { a: 1 }).filter).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("collects filters for default role", () => {
|
|
||||||
const p = new Permission(
|
|
||||||
"test",
|
|
||||||
{
|
|
||||||
filterable: true,
|
|
||||||
},
|
|
||||||
s.object({
|
|
||||||
a: s.number(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const r = new Role(
|
|
||||||
"test",
|
|
||||||
[
|
|
||||||
new RolePermission(p, [
|
|
||||||
new Policy({
|
|
||||||
condition: { a: { $eq: 1 } },
|
|
||||||
filter: { foo: "bar" },
|
|
||||||
effect: "filter",
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
const guard = new Guard([p], [r], {
|
|
||||||
enabled: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
guard.filters(
|
|
||||||
p,
|
|
||||||
{
|
|
||||||
role: r.name,
|
|
||||||
},
|
|
||||||
{ a: 1 },
|
|
||||||
).filter,
|
|
||||||
).toEqual({ foo: "bar" });
|
|
||||||
expect(
|
|
||||||
guard.filters(
|
|
||||||
p,
|
|
||||||
{
|
|
||||||
role: r.name,
|
|
||||||
},
|
|
||||||
{ a: 2 },
|
|
||||||
).filter,
|
|
||||||
).toBeUndefined();
|
|
||||||
// if no user context given, the default role is applied
|
|
||||||
// hence it can be found
|
|
||||||
expect(guard.filters(p, {}, { a: 1 }).filter).toEqual({ foo: "bar" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("merges filters correctly", () => {
|
|
||||||
expect(mergeFilters({ foo: "bar" }, { baz: "qux" })).toEqual({
|
|
||||||
foo: { $eq: "bar" },
|
|
||||||
baz: { $eq: "qux" },
|
|
||||||
});
|
|
||||||
expect(mergeFilters({ foo: "bar" }, { baz: { $eq: "qux" } })).toEqual({
|
|
||||||
foo: { $eq: "bar" },
|
|
||||||
baz: { $eq: "qux" },
|
|
||||||
});
|
|
||||||
expect(mergeFilters({ foo: "bar" }, { foo: "baz" })).toEqual({ foo: { $eq: "baz" } });
|
|
||||||
|
|
||||||
expect(mergeFilters({ foo: "bar" }, { foo: { $lt: 1 } })).toEqual({
|
|
||||||
foo: { $eq: "bar", $lt: 1 },
|
|
||||||
});
|
|
||||||
|
|
||||||
// overwrite base $or with priority
|
|
||||||
expect(mergeFilters({ $or: { foo: "one" } }, { foo: "bar" })).toEqual({
|
|
||||||
$or: {
|
|
||||||
foo: {
|
|
||||||
$eq: "bar",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
foo: {
|
|
||||||
$eq: "bar",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// ignore base $or if priority has different key
|
|
||||||
expect(mergeFilters({ $or: { other: "one" } }, { foo: "bar" })).toEqual({
|
|
||||||
$or: {
|
|
||||||
other: {
|
|
||||||
$eq: "one",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
foo: {
|
|
||||||
$eq: "bar",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("permission middleware", () => {
|
|
||||||
const makeApp = (
|
|
||||||
permissions: Permission<any, any, any, any>[],
|
|
||||||
roles: Role[] = [],
|
|
||||||
config: Partial<GuardConfig> = {},
|
|
||||||
) => {
|
|
||||||
const app = {
|
|
||||||
module: {
|
|
||||||
auth: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
modules: {
|
|
||||||
ctx: () => ({
|
|
||||||
guard: new Guard(permissions, roles, {
|
|
||||||
enabled: true,
|
|
||||||
...config,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return new Hono()
|
|
||||||
.use(async (c, next) => {
|
|
||||||
// @ts-expect-error
|
|
||||||
c.set("app", app);
|
|
||||||
await next();
|
|
||||||
})
|
|
||||||
.use(auth())
|
|
||||||
.onError((err, c) => {
|
|
||||||
if (err instanceof Exception) {
|
|
||||||
return c.json(err.toJSON(), err.code as any);
|
|
||||||
}
|
|
||||||
return c.json({ error: err.message }, "code" in err ? (err.code as any) : 500);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
it("allows if guard is disabled", async () => {
|
|
||||||
const p = new Permission("test");
|
|
||||||
const hono = makeApp([p], [], { enabled: false }).get("/test", permission(p, {}), async (c) =>
|
|
||||||
c.text("test"),
|
|
||||||
);
|
|
||||||
|
|
||||||
const res = await hono.request("/test");
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
expect(await res.text()).toBe("test");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("denies if guard is enabled", async () => {
|
|
||||||
const p = new Permission("test");
|
|
||||||
const hono = makeApp([p]).get("/test", permission(p, {}), async (c) => c.text("test"));
|
|
||||||
|
|
||||||
const res = await hono.request("/test");
|
|
||||||
expect(res.status).toBe(403);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows if user has (plain) role", async () => {
|
|
||||||
const p = new Permission("test");
|
|
||||||
const r = Role.create("test", { permissions: [p.name] });
|
|
||||||
const hono = makeApp([p], [r])
|
|
||||||
.use(async (c, next) => {
|
|
||||||
// @ts-expect-error
|
|
||||||
c.set("auth", { registered: true, user: { id: 0, role: r.name } });
|
|
||||||
await next();
|
|
||||||
})
|
|
||||||
.get("/test", permission(p, {}), async (c) => c.text("test"));
|
|
||||||
|
|
||||||
const res = await hono.request("/test");
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows if user has role with policy", async () => {
|
|
||||||
const p = new Permission("test");
|
|
||||||
const r = new Role("test", [
|
|
||||||
new RolePermission(p, [
|
|
||||||
new Policy({
|
|
||||||
condition: {
|
|
||||||
a: { $gte: 1 },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
const hono = makeApp([p], [r], {
|
|
||||||
context: {
|
|
||||||
a: 1,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.use(async (c, next) => {
|
|
||||||
// @ts-expect-error
|
|
||||||
c.set("auth", { registered: true, user: { id: 0, role: r.name } });
|
|
||||||
await next();
|
|
||||||
})
|
|
||||||
.get("/test", permission(p, {}), async (c) => c.text("test"));
|
|
||||||
|
|
||||||
const res = await hono.request("/test");
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("denies if user with role doesn't meet condition", async () => {
|
|
||||||
const p = new Permission("test");
|
|
||||||
const r = new Role("test", [
|
|
||||||
new RolePermission(
|
|
||||||
p,
|
|
||||||
[
|
|
||||||
new Policy({
|
|
||||||
condition: {
|
|
||||||
a: { $lt: 1 },
|
|
||||||
},
|
|
||||||
// default effect is allow
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
// change default effect to deny if no condition is met
|
|
||||||
"deny",
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
const hono = makeApp([p], [r], {
|
|
||||||
context: {
|
|
||||||
a: 1,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.use(async (c, next) => {
|
|
||||||
// @ts-expect-error
|
|
||||||
c.set("auth", { registered: true, user: { id: 0, role: r.name } });
|
|
||||||
await next();
|
|
||||||
})
|
|
||||||
.get("/test", permission(p, {}), async (c) => c.text("test"));
|
|
||||||
|
|
||||||
const res = await hono.request("/test");
|
|
||||||
expect(res.status).toBe(403);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows if user with role doesn't meet condition (from middleware)", async () => {
|
|
||||||
const p = new Permission(
|
|
||||||
"test",
|
|
||||||
{},
|
|
||||||
s.object({
|
|
||||||
a: s.number(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const r = new Role("test", [
|
|
||||||
new RolePermission(p, [
|
|
||||||
new Policy({
|
|
||||||
condition: {
|
|
||||||
a: { $eq: 1 },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
const hono = makeApp([p], [r])
|
|
||||||
.use(async (c, next) => {
|
|
||||||
// @ts-expect-error
|
|
||||||
c.set("auth", { registered: true, user: { id: 0, role: r.name } });
|
|
||||||
await next();
|
|
||||||
})
|
|
||||||
.get(
|
|
||||||
"/test",
|
|
||||||
permission(p, {
|
|
||||||
context: (c) => ({
|
|
||||||
a: 1,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (c) => c.text("test"),
|
|
||||||
);
|
|
||||||
|
|
||||||
const res = await hono.request("/test");
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws if permission context is invalid", async () => {
|
|
||||||
const p = new Permission(
|
|
||||||
"test",
|
|
||||||
{},
|
|
||||||
s.object({
|
|
||||||
a: s.number({ minimum: 2 }),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const r = new Role("test", [
|
|
||||||
new RolePermission(p, [
|
|
||||||
new Policy({
|
|
||||||
condition: {
|
|
||||||
a: { $eq: 1 },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
const hono = makeApp([p], [r])
|
|
||||||
.use(async (c, next) => {
|
|
||||||
// @ts-expect-error
|
|
||||||
c.set("auth", { registered: true, user: { id: 0, role: r.name } });
|
|
||||||
await next();
|
|
||||||
})
|
|
||||||
.get(
|
|
||||||
"/test",
|
|
||||||
permission(p, {
|
|
||||||
context: (c) => ({
|
|
||||||
a: 1,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (c) => c.text("test"),
|
|
||||||
);
|
|
||||||
|
|
||||||
const res = await hono.request("/test");
|
|
||||||
// expecting 500 because bknd should have handled it correctly
|
|
||||||
expect(res.status).toBe(500);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("checks context on routes with permissions", async () => {
|
|
||||||
const make = (user: any) => {
|
|
||||||
const p = new Permission(
|
|
||||||
"test",
|
|
||||||
{},
|
|
||||||
s.object({
|
|
||||||
a: s.number(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const r = new Role("test", [
|
|
||||||
new RolePermission(p, [
|
|
||||||
new Policy({
|
|
||||||
condition: {
|
|
||||||
a: { $eq: 1 },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
return makeApp([p], [r])
|
|
||||||
.use(async (c, next) => {
|
|
||||||
// @ts-expect-error
|
|
||||||
c.set("auth", { registered: true, user });
|
|
||||||
await next();
|
|
||||||
})
|
|
||||||
.get(
|
|
||||||
"/valid",
|
|
||||||
permission(p, {
|
|
||||||
context: (c) => ({
|
|
||||||
a: 1,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (c) => c.text("test"),
|
|
||||||
)
|
|
||||||
.get(
|
|
||||||
"/invalid",
|
|
||||||
permission(p, {
|
|
||||||
// @ts-expect-error
|
|
||||||
context: (c) => ({
|
|
||||||
b: "1",
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (c) => c.text("test"),
|
|
||||||
)
|
|
||||||
.get(
|
|
||||||
"/invalid2",
|
|
||||||
permission(p, {
|
|
||||||
// @ts-expect-error
|
|
||||||
context: (c) => ({}),
|
|
||||||
}),
|
|
||||||
async (c) => c.text("test"),
|
|
||||||
)
|
|
||||||
.get(
|
|
||||||
"/invalid3",
|
|
||||||
// @ts-expect-error
|
|
||||||
permission(p),
|
|
||||||
async (c) => c.text("test"),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const hono = make({ id: 0, role: "test" });
|
|
||||||
const valid = await hono.request("/valid");
|
|
||||||
expect(valid.status).toBe(200);
|
|
||||||
const invalid = await hono.request("/invalid");
|
|
||||||
expect(invalid.status).toBe(500);
|
|
||||||
const invalid2 = await hono.request("/invalid2");
|
|
||||||
expect(invalid2.status).toBe(500);
|
|
||||||
const invalid3 = await hono.request("/invalid3");
|
|
||||||
expect(invalid3.status).toBe(500);
|
|
||||||
|
|
||||||
{
|
|
||||||
const hono = make(null);
|
|
||||||
const valid = await hono.request("/valid");
|
|
||||||
expect(valid.status).toBe(403);
|
|
||||||
const invalid = await hono.request("/invalid");
|
|
||||||
expect(invalid.status).toBe(500);
|
|
||||||
const invalid2 = await hono.request("/invalid2");
|
|
||||||
expect(invalid2.status).toBe(500);
|
|
||||||
const invalid3 = await hono.request("/invalid3");
|
|
||||||
expect(invalid3.status).toBe(500);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Role", () => {
|
|
||||||
it("serializes and deserializes", () => {
|
|
||||||
const p = new Permission(
|
|
||||||
"test",
|
|
||||||
{
|
|
||||||
filterable: true,
|
|
||||||
},
|
|
||||||
s.object({
|
|
||||||
a: s.number({ minimum: 2 }),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const r = new Role(
|
|
||||||
"test",
|
|
||||||
[
|
|
||||||
new RolePermission(p, [
|
|
||||||
new Policy({
|
|
||||||
condition: {
|
|
||||||
a: { $eq: 1 },
|
|
||||||
},
|
|
||||||
effect: "deny",
|
|
||||||
filter: {
|
|
||||||
b: { $lt: 1 },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
const json = JSON.parse(JSON.stringify(r.toJSON()));
|
|
||||||
const r2 = Role.create(p.name, json);
|
|
||||||
expect(r2.toJSON()).toEqual(r.toJSON());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Registry } from "core/registry/Registry";
|
import { type TObject, type TString, Type } from "@sinclair/typebox";
|
||||||
import { s } from "core/utils/schema";
|
import { Registry } from "core";
|
||||||
|
|
||||||
type Constructor<T> = new (...args: any[]) => T;
|
type Constructor<T> = new (...args: any[]) => T;
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ class What {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
getType() {
|
getType() {
|
||||||
return s.object({ type: s.string() });
|
return Type.Object({ type: Type.String() });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class What2 extends What {}
|
class What2 extends What {}
|
||||||
@@ -19,7 +19,7 @@ class NotAllowed {}
|
|||||||
|
|
||||||
type Test1 = {
|
type Test1 = {
|
||||||
cls: new (...args: any[]) => What;
|
cls: new (...args: any[]) => What;
|
||||||
schema: s.ObjectSchema<{ type: s.StringSchema }>;
|
schema: TObject<{ type: TString }>;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ describe("Registry", () => {
|
|||||||
const registry = new Registry<Test1>().set({
|
const registry = new Registry<Test1>().set({
|
||||||
first: {
|
first: {
|
||||||
cls: What,
|
cls: What,
|
||||||
schema: s.object({ type: s.string(), what: s.string() }),
|
schema: Type.Object({ type: Type.String(), what: Type.String() }),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
} satisfies Record<string, Test1>);
|
} satisfies Record<string, Test1>);
|
||||||
@@ -37,7 +37,7 @@ describe("Registry", () => {
|
|||||||
expect(item).toBeDefined();
|
expect(item).toBeDefined();
|
||||||
expect(item?.cls).toBe(What);
|
expect(item?.cls).toBe(What);
|
||||||
|
|
||||||
const second = s.object({ type: s.string(), what: s.string() });
|
const second = Type.Object({ type: Type.String(), what: Type.String() });
|
||||||
registry.add("second", {
|
registry.add("second", {
|
||||||
cls: What2,
|
cls: What2,
|
||||||
schema: second,
|
schema: second,
|
||||||
@@ -46,7 +46,7 @@ describe("Registry", () => {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(registry.get("second").schema).toEqual(second);
|
expect(registry.get("second").schema).toEqual(second);
|
||||||
|
|
||||||
const third = s.object({ type: s.string({ default: "1" }), what22: s.string() });
|
const third = Type.Object({ type: Type.String({ default: "1" }), what22: Type.String() });
|
||||||
registry.add("third", {
|
registry.add("third", {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
cls: NotAllowed,
|
cls: NotAllowed,
|
||||||
@@ -56,7 +56,7 @@ describe("Registry", () => {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(registry.get("third").schema).toEqual(third);
|
expect(registry.get("third").schema).toEqual(third);
|
||||||
|
|
||||||
const fourth = s.object({ type: s.number(), what22: s.string() });
|
const fourth = Type.Object({ type: Type.Number(), what22: Type.String() });
|
||||||
registry.add("fourth", {
|
registry.add("fourth", {
|
||||||
cls: What,
|
cls: What,
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
@@ -81,8 +81,6 @@ describe("Registry", () => {
|
|||||||
registry.register("what2", What2);
|
registry.register("what2", What2);
|
||||||
expect(registry.get("what2")).toBeDefined();
|
expect(registry.get("what2")).toBeDefined();
|
||||||
expect(registry.get("what2").cls).toBe(What2);
|
expect(registry.get("what2").cls).toBe(What2);
|
||||||
expect(JSON.stringify(registry.get("what2").schema)).toEqual(
|
expect(registry.get("what2").schema).toEqual(What2.prototype.getType());
|
||||||
JSON.stringify(What2.prototype.getType()),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { s } from "core/utils/schema";
|
import { SchemaObject } from "../../../src/core";
|
||||||
import { SchemaObject } from "core/object/SchemaObject";
|
import { Type } from "@sinclair/typebox";
|
||||||
|
|
||||||
describe("SchemaObject", async () => {
|
describe("SchemaObject", async () => {
|
||||||
test("basic", async () => {
|
test("basic", async () => {
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
s.strictObject({ a: s.string({ default: "b" }) }),
|
Type.Object({ a: Type.String({ default: "b" }) }),
|
||||||
{ a: "test" },
|
{ a: "test" },
|
||||||
{
|
{
|
||||||
forceParse: true,
|
forceParse: true,
|
||||||
@@ -23,19 +23,19 @@ describe("SchemaObject", async () => {
|
|||||||
|
|
||||||
test("patch", async () => {
|
test("patch", async () => {
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
s.strictObject({
|
Type.Object({
|
||||||
s: s.strictObject(
|
s: Type.Object(
|
||||||
{
|
{
|
||||||
a: s.string({ default: "b" }),
|
a: Type.String({ default: "b" }),
|
||||||
b: s.strictObject(
|
b: Type.Object(
|
||||||
{
|
{
|
||||||
c: s.string({ default: "d" }),
|
c: Type.String({ default: "d" }),
|
||||||
e: s.string({ default: "f" }),
|
e: Type.String({ default: "f" }),
|
||||||
},
|
},
|
||||||
{ default: {} },
|
{ default: {} },
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{ default: {} },
|
{ default: {}, additionalProperties: false },
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -44,7 +44,7 @@ describe("SchemaObject", async () => {
|
|||||||
await m.patch("s.a", "c");
|
await m.patch("s.a", "c");
|
||||||
|
|
||||||
// non-existing path on no additional properties
|
// non-existing path on no additional properties
|
||||||
expect(m.patch("s.s.s", "c")).rejects.toThrow();
|
expect(() => m.patch("s.s.s", "c")).toThrow();
|
||||||
// wrong type
|
// wrong type
|
||||||
expect(() => m.patch("s.a", 1)).toThrow();
|
expect(() => m.patch("s.a", 1)).toThrow();
|
||||||
|
|
||||||
@@ -58,8 +58,8 @@ describe("SchemaObject", async () => {
|
|||||||
|
|
||||||
test("patch array", async () => {
|
test("patch array", async () => {
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
s.strictObject({
|
Type.Object({
|
||||||
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
|
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
|
||||||
@@ -75,13 +75,13 @@ describe("SchemaObject", async () => {
|
|||||||
|
|
||||||
test("remove", async () => {
|
test("remove", async () => {
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
s.object({
|
Type.Object({
|
||||||
s: s.object(
|
s: Type.Object(
|
||||||
{
|
{
|
||||||
a: s.string({ default: "b" }),
|
a: Type.String({ default: "b" }),
|
||||||
b: s.object(
|
b: Type.Object(
|
||||||
{
|
{
|
||||||
c: s.string({ default: "d" }),
|
c: Type.String({ default: "d" }),
|
||||||
},
|
},
|
||||||
{ default: {} },
|
{ default: {} },
|
||||||
),
|
),
|
||||||
@@ -107,8 +107,8 @@ describe("SchemaObject", async () => {
|
|||||||
|
|
||||||
test("set", async () => {
|
test("set", async () => {
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
s.strictObject({
|
Type.Object({
|
||||||
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
|
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
|
||||||
@@ -124,8 +124,8 @@ describe("SchemaObject", async () => {
|
|||||||
let called = false;
|
let called = false;
|
||||||
let result: any;
|
let result: any;
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
s.strictObject({
|
Type.Object({
|
||||||
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
|
||||||
}),
|
}),
|
||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
@@ -145,8 +145,8 @@ describe("SchemaObject", async () => {
|
|||||||
test("listener: onBeforeUpdate", async () => {
|
test("listener: onBeforeUpdate", async () => {
|
||||||
let called = false;
|
let called = false;
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
s.strictObject({
|
Type.Object({
|
||||||
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
|
||||||
}),
|
}),
|
||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
@@ -167,7 +167,7 @@ describe("SchemaObject", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("throwIfRestricted", async () => {
|
test("throwIfRestricted", async () => {
|
||||||
const m = new SchemaObject(s.strictObject({}), undefined, {
|
const m = new SchemaObject(Type.Object({}), undefined, {
|
||||||
restrictPaths: ["a.b"],
|
restrictPaths: ["a.b"],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -179,13 +179,13 @@ describe("SchemaObject", async () => {
|
|||||||
|
|
||||||
test("restriction bypass", async () => {
|
test("restriction bypass", async () => {
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
s.strictObject({
|
Type.Object({
|
||||||
s: s.strictObject(
|
s: Type.Object(
|
||||||
{
|
{
|
||||||
a: s.string({ default: "b" }),
|
a: Type.String({ default: "b" }),
|
||||||
b: s.strictObject(
|
b: Type.Object(
|
||||||
{
|
{
|
||||||
c: s.string({ default: "d" }),
|
c: Type.String({ default: "d" }),
|
||||||
},
|
},
|
||||||
{ default: {} },
|
{ default: {} },
|
||||||
),
|
),
|
||||||
@@ -205,21 +205,7 @@ describe("SchemaObject", async () => {
|
|||||||
expect(m.get()).toEqual({ s: { a: "b", b: { c: "e" } } });
|
expect(m.get()).toEqual({ s: { a: "b", b: { c: "e" } } });
|
||||||
});
|
});
|
||||||
|
|
||||||
const dataEntitiesSchema = s.strictObject({
|
const dataEntitiesSchema = Type.Object(
|
||||||
entities: s.record(
|
|
||||||
s.object({
|
|
||||||
fields: s.record(
|
|
||||||
s.object({
|
|
||||||
type: s.string(),
|
|
||||||
config: s.object({}).optional(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
config: s.record(s.string()).optional(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
/* const dataEntitiesSchema = Type.Object(
|
|
||||||
{
|
{
|
||||||
entities: Type.Object(
|
entities: Type.Object(
|
||||||
{},
|
{},
|
||||||
@@ -244,7 +230,7 @@ describe("SchemaObject", async () => {
|
|||||||
{
|
{
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
},
|
},
|
||||||
); */
|
);
|
||||||
test("patch safe object, overwrite", async () => {
|
test("patch safe object, overwrite", async () => {
|
||||||
const data = {
|
const data = {
|
||||||
entities: {
|
entities: {
|
||||||
|
|||||||
@@ -66,14 +66,4 @@ describe("object-query", () => {
|
|||||||
expect(result).toBe(expected);
|
expect(result).toBe(expected);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("paths", () => {
|
|
||||||
const result = validate({ "user.age": { $lt: 18 } }, { user: { age: 17 } });
|
|
||||||
expect(result).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("empty filters", () => {
|
|
||||||
const result = validate({}, { user: { age: 17 } });
|
|
||||||
expect(result).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
|
||||||
import {
|
|
||||||
makeValidator,
|
|
||||||
exp,
|
|
||||||
Expression,
|
|
||||||
isPrimitive,
|
|
||||||
type Primitive,
|
|
||||||
} from "../../../src/core/object/query/query";
|
|
||||||
|
|
||||||
describe("query", () => {
|
|
||||||
test("isPrimitive", () => {
|
|
||||||
expect(isPrimitive(1)).toBe(true);
|
|
||||||
expect(isPrimitive("1")).toBe(true);
|
|
||||||
expect(isPrimitive(true)).toBe(true);
|
|
||||||
expect(isPrimitive(false)).toBe(true);
|
|
||||||
|
|
||||||
// not primitives
|
|
||||||
expect(isPrimitive(null)).toBe(false);
|
|
||||||
expect(isPrimitive(undefined)).toBe(false);
|
|
||||||
expect(isPrimitive([])).toBe(false);
|
|
||||||
expect(isPrimitive({})).toBe(false);
|
|
||||||
expect(isPrimitive(Symbol("test"))).toBe(false);
|
|
||||||
expect(isPrimitive(new Date())).toBe(false);
|
|
||||||
expect(isPrimitive(new Error())).toBe(false);
|
|
||||||
expect(isPrimitive(new Set())).toBe(false);
|
|
||||||
expect(isPrimitive(new Map())).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("strict expression creation", () => {
|
|
||||||
// @ts-expect-error
|
|
||||||
expect(() => exp()).toThrow();
|
|
||||||
// @ts-expect-error
|
|
||||||
expect(() => exp("")).toThrow();
|
|
||||||
// @ts-expect-error
|
|
||||||
expect(() => exp("invalid")).toThrow();
|
|
||||||
// @ts-expect-error
|
|
||||||
expect(() => exp("$eq")).toThrow();
|
|
||||||
// @ts-expect-error
|
|
||||||
expect(() => exp("$eq", 1)).toThrow();
|
|
||||||
// @ts-expect-error
|
|
||||||
expect(() => exp("$eq", () => null)).toThrow();
|
|
||||||
// @ts-expect-error
|
|
||||||
expect(() => exp("$eq", () => null, 1)).toThrow();
|
|
||||||
expect(
|
|
||||||
exp(
|
|
||||||
"$eq",
|
|
||||||
() => true,
|
|
||||||
() => null,
|
|
||||||
),
|
|
||||||
).toBeInstanceOf(Expression);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("$eq is required", () => {
|
|
||||||
expect(() => makeValidator([])).toThrow();
|
|
||||||
expect(() =>
|
|
||||||
makeValidator([
|
|
||||||
exp(
|
|
||||||
"$valid",
|
|
||||||
() => true,
|
|
||||||
() => null,
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
).toThrow();
|
|
||||||
expect(
|
|
||||||
makeValidator([
|
|
||||||
exp(
|
|
||||||
"$eq",
|
|
||||||
() => true,
|
|
||||||
() => null,
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("validates filter structure", () => {
|
|
||||||
const validator = makeValidator([
|
|
||||||
exp(
|
|
||||||
"$eq",
|
|
||||||
(v: Primitive) => isPrimitive(v),
|
|
||||||
(e, a) => e === a,
|
|
||||||
),
|
|
||||||
exp(
|
|
||||||
"$like",
|
|
||||||
(v: string) => typeof v === "string",
|
|
||||||
(e, a) => e === a,
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// @ts-expect-error intentionally typed as union of given expression keys
|
|
||||||
expect(validator.expressionKeys).toEqual(["$eq", "$like"]);
|
|
||||||
|
|
||||||
// @ts-expect-error "$and" is not allowed
|
|
||||||
expect(() => validator.convert({ $and: {} })).toThrow();
|
|
||||||
|
|
||||||
// @ts-expect-error "$or" must be an object
|
|
||||||
expect(() => validator.convert({ $or: [] })).toThrow();
|
|
||||||
|
|
||||||
// @ts-expect-error "invalid" is not a valid expression key
|
|
||||||
expect(() => validator.convert({ foo: { invalid: "bar" } })).toThrow();
|
|
||||||
|
|
||||||
// @ts-expect-error "invalid" is not a valid expression key
|
|
||||||
expect(() => validator.convert({ foo: { $invalid: "bar" } })).toThrow();
|
|
||||||
|
|
||||||
// @ts-expect-error "null" is not a valid value
|
|
||||||
expect(() => validator.convert({ foo: null })).toThrow();
|
|
||||||
|
|
||||||
// @ts-expect-error only primitives are allowed for $eq
|
|
||||||
expect(() => validator.convert({ foo: { $eq: [] } })).toThrow();
|
|
||||||
|
|
||||||
// @ts-expect-error only strings are allowed for $like
|
|
||||||
expect(() => validator.convert({ foo: { $like: 1 } })).toThrow();
|
|
||||||
|
|
||||||
// undefined values are ignored
|
|
||||||
expect(validator.convert({ foo: undefined })).toEqual({});
|
|
||||||
|
|
||||||
expect(validator.convert({ foo: "bar" })).toEqual({ foo: { $eq: "bar" } });
|
|
||||||
expect(validator.convert({ foo: { $eq: "bar" } })).toEqual({ foo: { $eq: "bar" } });
|
|
||||||
expect(validator.convert({ foo: { $like: "bar" } })).toEqual({ foo: { $like: "bar" } });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -194,182 +194,6 @@ describe("Core Utils", async () => {
|
|||||||
expect(result).toEqual(expected);
|
expect(result).toEqual(expected);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("recursivelyReplacePlaceholders", () => {
|
|
||||||
// test basic replacement with simple pattern
|
|
||||||
const obj1 = { a: "Hello, {$name}!", b: { c: "Hello, {$name}!" } };
|
|
||||||
const variables1 = { name: "John" };
|
|
||||||
const result1 = utils.recursivelyReplacePlaceholders(obj1, /\{\$(\w+)\}/g, variables1);
|
|
||||||
expect(result1).toEqual({ a: "Hello, John!", b: { c: "Hello, John!" } });
|
|
||||||
|
|
||||||
// test the specific example from the user request
|
|
||||||
const obj2 = { some: "value", here: "@auth.user" };
|
|
||||||
const variables2 = { auth: { user: "what" } };
|
|
||||||
const result2 = utils.recursivelyReplacePlaceholders(obj2, /^@([a-z\.]+)$/, variables2);
|
|
||||||
expect(result2).toEqual({ some: "value", here: "what" });
|
|
||||||
|
|
||||||
// test with arrays
|
|
||||||
const obj3 = { items: ["@config.name", "static", "@config.version"] };
|
|
||||||
const variables3 = { config: { name: "MyApp", version: "1.0.0" } };
|
|
||||||
const result3 = utils.recursivelyReplacePlaceholders(obj3, /^@([a-z\.]+)$/, variables3);
|
|
||||||
expect(result3).toEqual({ items: ["MyApp", "static", "1.0.0"] });
|
|
||||||
|
|
||||||
// test with nested objects and deep paths
|
|
||||||
const obj4 = {
|
|
||||||
user: "@auth.user.name",
|
|
||||||
settings: {
|
|
||||||
theme: "@ui.theme",
|
|
||||||
nested: {
|
|
||||||
value: "@deep.nested.value",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const variables4 = {
|
|
||||||
auth: { user: { name: "Alice" } },
|
|
||||||
ui: { theme: "dark" },
|
|
||||||
deep: { nested: { value: "found" } },
|
|
||||||
};
|
|
||||||
const result4 = utils.recursivelyReplacePlaceholders(obj4, /^@([a-z\.]+)$/, variables4);
|
|
||||||
expect(result4).toEqual({
|
|
||||||
user: "Alice",
|
|
||||||
settings: {
|
|
||||||
theme: "dark",
|
|
||||||
nested: {
|
|
||||||
value: "found",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// test with missing paths (should return original match)
|
|
||||||
const obj5 = { value: "@missing.path" };
|
|
||||||
const variables5 = { existing: "value" };
|
|
||||||
const result5 = utils.recursivelyReplacePlaceholders(obj5, /^@([a-z\.]+)$/, variables5);
|
|
||||||
expect(result5).toEqual({ value: "@missing.path" });
|
|
||||||
|
|
||||||
// test with non-matching strings (should remain unchanged)
|
|
||||||
const obj6 = { value: "normal string", other: "not@matching" };
|
|
||||||
const variables6 = { some: "value" };
|
|
||||||
const result6 = utils.recursivelyReplacePlaceholders(obj6, /^@([a-z\.]+)$/, variables6);
|
|
||||||
expect(result6).toEqual({ value: "normal string", other: "not@matching" });
|
|
||||||
|
|
||||||
// test with primitive values (should handle gracefully)
|
|
||||||
expect(
|
|
||||||
utils.recursivelyReplacePlaceholders("@test.value", /^@([a-z\.]+)$/, {
|
|
||||||
test: { value: "replaced" },
|
|
||||||
}),
|
|
||||||
).toBe("replaced");
|
|
||||||
expect(utils.recursivelyReplacePlaceholders(123, /^@([a-z\.]+)$/, {})).toBe(123);
|
|
||||||
expect(utils.recursivelyReplacePlaceholders(null, /^@([a-z\.]+)$/, {})).toBe(null);
|
|
||||||
|
|
||||||
// test type preservation for full string matches
|
|
||||||
const variables7 = { test: { value: 123, flag: true, data: null, arr: [1, 2, 3] } };
|
|
||||||
const result7 = utils.recursivelyReplacePlaceholders(
|
|
||||||
{
|
|
||||||
number: "@test.value",
|
|
||||||
boolean: "@test.flag",
|
|
||||||
nullValue: "@test.data",
|
|
||||||
array: "@test.arr",
|
|
||||||
},
|
|
||||||
/^@([a-z\.]+)$/,
|
|
||||||
variables7,
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
expect(result7).toEqual({
|
|
||||||
number: 123,
|
|
||||||
boolean: true,
|
|
||||||
nullValue: null,
|
|
||||||
array: [1, 2, 3],
|
|
||||||
});
|
|
||||||
|
|
||||||
// test partial string replacement (should convert to string)
|
|
||||||
const result8 = utils.recursivelyReplacePlaceholders(
|
|
||||||
{ message: "The value is @test.value!" },
|
|
||||||
/@([a-z\.]+)/g,
|
|
||||||
variables7,
|
|
||||||
);
|
|
||||||
expect(result8).toEqual({ message: "The value is 123!" });
|
|
||||||
|
|
||||||
// test with fallback parameter
|
|
||||||
const obj9 = { user: "@user.id", config: "@config.theme" };
|
|
||||||
const variables9 = {}; // empty context
|
|
||||||
const result9 = utils.recursivelyReplacePlaceholders(
|
|
||||||
obj9,
|
|
||||||
/^@([a-z\.]+)$/,
|
|
||||||
variables9,
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
expect(result9).toEqual({ user: null, config: null });
|
|
||||||
|
|
||||||
// test with fallback for partial matches
|
|
||||||
const obj10 = { message: "Hello @user.name, welcome!" };
|
|
||||||
const variables10 = {}; // empty context
|
|
||||||
const result10 = utils.recursivelyReplacePlaceholders(
|
|
||||||
obj10,
|
|
||||||
/@([a-z\.]+)/g,
|
|
||||||
variables10,
|
|
||||||
"Guest",
|
|
||||||
);
|
|
||||||
expect(result10).toEqual({ message: "Hello Guest, welcome!" });
|
|
||||||
|
|
||||||
// test with different fallback types
|
|
||||||
const obj11 = {
|
|
||||||
stringFallback: "@missing.string",
|
|
||||||
numberFallback: "@missing.number",
|
|
||||||
booleanFallback: "@missing.boolean",
|
|
||||||
objectFallback: "@missing.object",
|
|
||||||
};
|
|
||||||
const variables11 = {};
|
|
||||||
const result11 = utils.recursivelyReplacePlaceholders(
|
|
||||||
obj11,
|
|
||||||
/^@([a-z\.]+)$/,
|
|
||||||
variables11,
|
|
||||||
"default",
|
|
||||||
);
|
|
||||||
expect(result11).toEqual({
|
|
||||||
stringFallback: "default",
|
|
||||||
numberFallback: "default",
|
|
||||||
booleanFallback: "default",
|
|
||||||
objectFallback: "default",
|
|
||||||
});
|
|
||||||
|
|
||||||
// test fallback with arrays
|
|
||||||
const obj12 = { items: ["@item1", "@item2", "static"] };
|
|
||||||
const variables12 = { item1: "found" }; // item2 is missing
|
|
||||||
const result12 = utils.recursivelyReplacePlaceholders(
|
|
||||||
obj12,
|
|
||||||
/^@([a-zA-Z0-9\.]+)$/,
|
|
||||||
variables12,
|
|
||||||
"missing",
|
|
||||||
);
|
|
||||||
expect(result12).toEqual({ items: ["found", "missing", "static"] });
|
|
||||||
|
|
||||||
// test fallback with nested objects
|
|
||||||
const obj13 = {
|
|
||||||
user: "@user.id",
|
|
||||||
settings: {
|
|
||||||
theme: "@theme.name",
|
|
||||||
nested: {
|
|
||||||
value: "@deep.value",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const variables13 = {}; // empty context
|
|
||||||
const result13 = utils.recursivelyReplacePlaceholders(
|
|
||||||
obj13,
|
|
||||||
/^@([a-z\.]+)$/,
|
|
||||||
variables13,
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
expect(result13).toEqual({
|
|
||||||
user: null,
|
|
||||||
settings: {
|
|
||||||
theme: null,
|
|
||||||
nested: {
|
|
||||||
value: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("file", async () => {
|
describe("file", async () => {
|
||||||
@@ -424,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();
|
||||||
@@ -440,44 +264,15 @@ describe("Core Utils", async () => {
|
|||||||
height: 512,
|
height: 512,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("isFileAccepted", () => {
|
|
||||||
const file = new File([""], "file.txt", {
|
|
||||||
type: "text/plain",
|
|
||||||
});
|
|
||||||
expect(utils.isFileAccepted(file, "text/plain")).toBe(true);
|
|
||||||
expect(utils.isFileAccepted(file, "text/plain,text/html")).toBe(true);
|
|
||||||
expect(utils.isFileAccepted(file, "text/html")).toBe(false);
|
|
||||||
|
|
||||||
{
|
|
||||||
const file = new File([""], "file.jpg", {
|
|
||||||
type: "image/jpeg",
|
|
||||||
});
|
|
||||||
expect(utils.isFileAccepted(file, "image/jpeg")).toBe(true);
|
|
||||||
expect(utils.isFileAccepted(file, "image/jpeg,image/png")).toBe(true);
|
|
||||||
expect(utils.isFileAccepted(file, "image/png")).toBe(false);
|
|
||||||
expect(utils.isFileAccepted(file, "image/*")).toBe(true);
|
|
||||||
expect(utils.isFileAccepted(file, ".jpg")).toBe(true);
|
|
||||||
expect(utils.isFileAccepted(file, ".jpg,.png")).toBe(true);
|
|
||||||
expect(utils.isFileAccepted(file, ".png")).toBe(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const file = new File([""], "file.png");
|
|
||||||
expect(utils.isFileAccepted(file, undefined as any)).toBe(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(() => utils.isFileAccepted(null as any, "text/plain")).toThrow();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,24 +1,72 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
import { Guard } from "../../src/auth/authorize/Guard";
|
import { Guard } from "../../src/auth";
|
||||||
import { parse } from "core/utils/schema";
|
import { parse } from "../../src/core/utils";
|
||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
type EntityData,
|
||||||
|
EntityManager,
|
||||||
|
ManyToOneRelation,
|
||||||
|
type MutatorResponse,
|
||||||
|
type RepositoryResponse,
|
||||||
|
TextField,
|
||||||
|
} from "../../src/data";
|
||||||
import { DataController } from "../../src/data/api/DataController";
|
import { DataController } from "../../src/data/api/DataController";
|
||||||
import { dataConfigSchema } from "../../src/data/data-schema";
|
import { dataConfigSchema } from "../../src/data/data-schema";
|
||||||
import { 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 { MutatorResultJSON } from "data/entities/mutation/MutatorResult";
|
|
||||||
import { Entity, EntityManager, type EntityData } from "data/entities";
|
|
||||||
import { TextField } from "data/fields";
|
|
||||||
import { ManyToOneRelation } from "data/relations";
|
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
beforeAll(() => disableConsoleLog());
|
beforeAll(() => disableConsoleLog(["log", "warn"]));
|
||||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||||
|
|
||||||
const dataConfig = parse(dataConfigSchema, {});
|
const dataConfig = parse(dataConfigSchema, {});
|
||||||
describe("[data] DataController", async () => {
|
describe("[data] DataController", async () => {
|
||||||
|
test("repoResult", async () => {
|
||||||
|
const em = new EntityManager<any>([], dummyConnection);
|
||||||
|
const ctx: any = { em, guard: new Guard() };
|
||||||
|
const controller = new DataController(ctx, dataConfig);
|
||||||
|
|
||||||
|
const res = controller.repoResult({
|
||||||
|
entity: null as any,
|
||||||
|
data: [] as any,
|
||||||
|
sql: "",
|
||||||
|
parameters: [] as any,
|
||||||
|
result: [] as any,
|
||||||
|
meta: {
|
||||||
|
total: 0,
|
||||||
|
count: 0,
|
||||||
|
items: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res).toEqual({
|
||||||
|
meta: {
|
||||||
|
total: 0,
|
||||||
|
count: 0,
|
||||||
|
items: 0,
|
||||||
|
},
|
||||||
|
data: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("mutatorResult", async () => {
|
||||||
|
const em = new EntityManager([], dummyConnection);
|
||||||
|
const ctx: any = { em, guard: new Guard() };
|
||||||
|
const controller = new DataController(ctx, dataConfig);
|
||||||
|
|
||||||
|
const res = controller.mutatorResult({
|
||||||
|
entity: null as any,
|
||||||
|
data: [] as any,
|
||||||
|
sql: "",
|
||||||
|
parameters: [] as any,
|
||||||
|
result: [] as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res).toEqual({
|
||||||
|
data: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("getController", async () => {
|
describe("getController", async () => {
|
||||||
const users = new Entity("users", [
|
const users = new Entity("users", [
|
||||||
new TextField("name", { required: true }),
|
new TextField("name", { required: true }),
|
||||||
@@ -72,7 +120,8 @@ describe("[data] DataController", async () => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(_user),
|
body: JSON.stringify(_user),
|
||||||
});
|
});
|
||||||
const result = (await res.json()) as MutatorResultJSON;
|
//console.log("res", { _user }, res);
|
||||||
|
const result = (await res.json()) as MutatorResponse;
|
||||||
const { id, ...data } = result.data as any;
|
const { id, ...data } = result.data as any;
|
||||||
|
|
||||||
expect(res.status).toBe(201);
|
expect(res.status).toBe(201);
|
||||||
@@ -86,7 +135,7 @@ describe("[data] DataController", async () => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(_post),
|
body: JSON.stringify(_post),
|
||||||
});
|
});
|
||||||
const result = (await res.json()) as MutatorResultJSON;
|
const result = (await res.json()) as MutatorResponse;
|
||||||
const { id, ...data } = result.data as any;
|
const { id, ...data } = result.data as any;
|
||||||
|
|
||||||
expect(res.status).toBe(201);
|
expect(res.status).toBe(201);
|
||||||
@@ -97,13 +146,13 @@ describe("[data] DataController", async () => {
|
|||||||
|
|
||||||
test("/:entity (read many)", async () => {
|
test("/:entity (read many)", async () => {
|
||||||
const res = await app.request("/entity/users");
|
const res = await app.request("/entity/users");
|
||||||
const data = (await res.json()) as RepositoryResultJSON;
|
const data = (await res.json()) as RepositoryResponse;
|
||||||
|
|
||||||
//expect(data.meta.total).toBe(3);
|
expect(data.meta.total).toBe(3);
|
||||||
//expect(data.meta.count).toBe(3);
|
expect(data.meta.count).toBe(3);
|
||||||
expect(data.meta.items).toBe(3);
|
expect(data.meta.items).toBe(3);
|
||||||
expect(data.data.length).toBe(3);
|
expect(data.data.length).toBe(3);
|
||||||
expect(data.data[0]?.name).toBe("foo");
|
expect(data.data[0].name).toBe("foo");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("/:entity/query (func query)", async () => {
|
test("/:entity/query (func query)", async () => {
|
||||||
@@ -116,32 +165,33 @@ describe("[data] DataController", async () => {
|
|||||||
where: { bio: { $isnull: 1 } },
|
where: { bio: { $isnull: 1 } },
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const data = (await res.json()) as RepositoryResultJSON;
|
const data = (await res.json()) as RepositoryResponse;
|
||||||
|
|
||||||
//expect(data.meta.total).toBe(3);
|
expect(data.meta.total).toBe(3);
|
||||||
//expect(data.meta.count).toBe(1);
|
expect(data.meta.count).toBe(1);
|
||||||
expect(data.meta.items).toBe(1);
|
expect(data.meta.items).toBe(1);
|
||||||
expect(data.data.length).toBe(1);
|
expect(data.data.length).toBe(1);
|
||||||
expect(data.data[0]?.name).toBe("bar");
|
expect(data.data[0].name).toBe("bar");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("/:entity (read many, paginated)", async () => {
|
test("/:entity (read many, paginated)", async () => {
|
||||||
const res = await app.request("/entity/users?limit=1&offset=2");
|
const res = await app.request("/entity/users?limit=1&offset=2");
|
||||||
const data = (await res.json()) as RepositoryResultJSON;
|
const data = (await res.json()) as RepositoryResponse;
|
||||||
|
|
||||||
//expect(data.meta.total).toBe(3);
|
expect(data.meta.total).toBe(3);
|
||||||
//expect(data.meta.count).toBe(3);
|
expect(data.meta.count).toBe(3);
|
||||||
expect(data.meta.items).toBe(1);
|
expect(data.meta.items).toBe(1);
|
||||||
expect(data.data.length).toBe(1);
|
expect(data.data.length).toBe(1);
|
||||||
expect(data.data[0]?.name).toBe("baz");
|
expect(data.data[0].name).toBe("baz");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("/:entity/:id (read one)", async () => {
|
test("/:entity/:id (read one)", async () => {
|
||||||
const res = await app.request("/entity/users/3");
|
const res = await app.request("/entity/users/3");
|
||||||
const data = (await res.json()) as RepositoryResultJSON<EntityData>;
|
const data = (await res.json()) as RepositoryResponse<EntityData>;
|
||||||
|
console.log("data", data);
|
||||||
|
|
||||||
//expect(data.meta.total).toBe(3);
|
expect(data.meta.total).toBe(3);
|
||||||
//expect(data.meta.count).toBe(1);
|
expect(data.meta.count).toBe(1);
|
||||||
expect(data.meta.items).toBe(1);
|
expect(data.meta.items).toBe(1);
|
||||||
expect(data.data).toEqual({ id: 3, ...fixtures.users[2] });
|
expect(data.data).toEqual({ id: 3, ...fixtures.users[2] });
|
||||||
});
|
});
|
||||||
@@ -151,7 +201,7 @@ describe("[data] DataController", async () => {
|
|||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: JSON.stringify({ name: "new name" }),
|
body: JSON.stringify({ name: "new name" }),
|
||||||
});
|
});
|
||||||
const { data } = (await res.json()) as MutatorResultJSON;
|
const { data } = (await res.json()) as MutatorResponse;
|
||||||
|
|
||||||
expect(res.ok).toBe(true);
|
expect(res.ok).toBe(true);
|
||||||
expect(data as any).toEqual({ id: 3, ...fixtures.users[2], name: "new name" });
|
expect(data as any).toEqual({ id: 3, ...fixtures.users[2], name: "new name" });
|
||||||
@@ -159,26 +209,27 @@ describe("[data] DataController", async () => {
|
|||||||
|
|
||||||
test("/:entity/:id/:reference (read references)", async () => {
|
test("/:entity/:id/:reference (read references)", async () => {
|
||||||
const res = await app.request("/entity/users/1/posts");
|
const res = await app.request("/entity/users/1/posts");
|
||||||
const data = (await res.json()) as RepositoryResultJSON;
|
const data = (await res.json()) as RepositoryResponse;
|
||||||
|
console.log("data", data);
|
||||||
|
|
||||||
//expect(data.meta.total).toBe(2);
|
expect(data.meta.total).toBe(2);
|
||||||
//expect(data.meta.count).toBe(1);
|
expect(data.meta.count).toBe(1);
|
||||||
expect(data.meta.items).toBe(1);
|
expect(data.meta.items).toBe(1);
|
||||||
expect(data.data.length).toBe(1);
|
expect(data.data.length).toBe(1);
|
||||||
expect(data.data[0]?.content).toBe("post 1");
|
expect(data.data[0].content).toBe("post 1");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("/:entity/:id (delete one)", async () => {
|
test("/:entity/:id (delete one)", async () => {
|
||||||
const res = await app.request("/entity/posts/2", {
|
const res = await app.request("/entity/posts/2", {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
const { data } = (await res.json()) as RepositoryResultJSON<EntityData>;
|
const { data } = (await res.json()) as RepositoryResponse<EntityData>;
|
||||||
expect(data).toEqual({ id: 2, ...fixtures.posts[1] });
|
expect(data).toEqual({ id: 2, ...fixtures.posts[1] });
|
||||||
|
|
||||||
// verify
|
// verify
|
||||||
const res2 = await app.request("/entity/posts");
|
const res2 = await app.request("/entity/posts");
|
||||||
const data2 = (await res2.json()) as RepositoryResultJSON;
|
const data2 = (await res2.json()) as RepositoryResponse;
|
||||||
//expect(data2.meta.total).toBe(1);
|
expect(data2.meta.total).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { Entity, EntityManager } from "data/entities";
|
import {
|
||||||
import { TextField, PrimaryField, NumberField } from "data/fields";
|
Entity,
|
||||||
|
EntityManager,
|
||||||
|
NumberField,
|
||||||
|
PrimaryField,
|
||||||
|
Repository,
|
||||||
|
TextField,
|
||||||
|
} from "../../src/data";
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
@@ -28,12 +34,19 @@ describe("some tests", async () => {
|
|||||||
|
|
||||||
test("findId", async () => {
|
test("findId", async () => {
|
||||||
const query = await em.repository(users).findId(1);
|
const query = await em.repository(users).findId(1);
|
||||||
|
/*const { result, total, count, time } = query;
|
||||||
|
console.log("query", query.result, {
|
||||||
|
result,
|
||||||
|
total,
|
||||||
|
count,
|
||||||
|
time,
|
||||||
|
});*/
|
||||||
|
|
||||||
expect(query.sql).toBe(
|
expect(query.sql).toBe(
|
||||||
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" where "id" = ? order by "users"."id" asc limit ? offset ?',
|
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" where "id" = ? limit ?',
|
||||||
);
|
);
|
||||||
expect(query.parameters).toEqual([1, 1, 0]);
|
expect(query.parameters).toEqual([1, 1]);
|
||||||
expect(query.data).toBeUndefined();
|
expect(query.result).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("findMany", async () => {
|
test("findMany", async () => {
|
||||||
@@ -43,7 +56,7 @@ describe("some tests", async () => {
|
|||||||
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" order by "users"."id" asc limit ? offset ?',
|
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" order by "users"."id" asc limit ? offset ?',
|
||||||
);
|
);
|
||||||
expect(query.parameters).toEqual([10, 0]);
|
expect(query.parameters).toEqual([10, 0]);
|
||||||
expect(query.data).toEqual([]);
|
expect(query.result).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("findMany with number", async () => {
|
test("findMany with number", async () => {
|
||||||
@@ -53,7 +66,7 @@ describe("some tests", async () => {
|
|||||||
'select "posts"."id" as "id", "posts"."title" as "title", "posts"."content" as "content", "posts"."created_at" as "created_at", "posts"."likes" as "likes" from "posts" order by "posts"."id" asc limit ? offset ?',
|
'select "posts"."id" as "id", "posts"."title" as "title", "posts"."content" as "content", "posts"."created_at" as "created_at", "posts"."likes" as "likes" from "posts" order by "posts"."id" asc limit ? offset ?',
|
||||||
);
|
);
|
||||||
expect(query.parameters).toEqual([10, 0]);
|
expect(query.parameters).toEqual([10, 0]);
|
||||||
expect(query.data).toEqual([]);
|
expect(query.result).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("try adding an existing field name", async () => {
|
test("try adding an existing field name", async () => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { unlink } from "node:fs/promises";
|
|||||||
import type { SqliteDatabase } from "kysely";
|
import type { SqliteDatabase } from "kysely";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import Database from "libsql";
|
import Database from "libsql";
|
||||||
import { SqliteLocalConnection } from "data/connection/sqlite/SqliteLocalConnection";
|
import { SqliteLocalConnection } from "../../src/data";
|
||||||
|
|
||||||
export function getDummyDatabase(memory: boolean = true): {
|
export function getDummyDatabase(memory: boolean = true): {
|
||||||
dummyDb: SqliteDatabase;
|
dummyDb: SqliteDatabase;
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
// eslint-disable-next-line import/no-unresolved
|
// eslint-disable-next-line import/no-unresolved
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { Entity } from "data/entities";
|
import {
|
||||||
import { EntityManager } from "data/entities/EntityManager";
|
Entity,
|
||||||
import { ManyToOneRelation } from "data/relations";
|
EntityManager,
|
||||||
import { NumberField, TextField } from "data/fields";
|
ManyToOneRelation,
|
||||||
import { SchemaManager } from "data/schema/SchemaManager";
|
NumberField,
|
||||||
|
SchemaManager,
|
||||||
|
TextField,
|
||||||
|
} from "../../src/data";
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
// eslint-disable-next-line import/no-unresolved
|
// eslint-disable-next-line import/no-unresolved
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { Entity, EntityManager } from "data/entities";
|
import { Entity, EntityManager, Mutator, NumberField, TextField } from "../../src/data";
|
||||||
import { NumberField, TextField } from "data/fields";
|
import { TransformPersistFailedException } from "../../src/data/errors";
|
||||||
import { TransformPersistFailedException } from "data/errors";
|
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
@@ -46,7 +45,7 @@ describe("Mutator simple", async () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(query.data).toEqual([{ id: 1, label: "test", count: 1 }]);
|
expect(query.result).toEqual([{ id: 1, label: "test", count: 1 }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("update inserted row", async () => {
|
test("update inserted row", async () => {
|
||||||
@@ -88,7 +87,7 @@ describe("Mutator simple", async () => {
|
|||||||
expect(mutation.data).toEqual({ id, label: "new label", count: 100 });
|
expect(mutation.data).toEqual({ id, label: "new label", count: 100 });
|
||||||
|
|
||||||
const query2 = await em.repository(items).findId(id);
|
const query2 = await em.repository(items).findId(id);
|
||||||
expect(query2.data).toBeUndefined();
|
expect(query2.result.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("validation: insert incomplete row", async () => {
|
test("validation: insert incomplete row", async () => {
|
||||||
@@ -178,13 +177,13 @@ describe("Mutator simple", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("insertMany", async () => {
|
test("insertMany", async () => {
|
||||||
const oldCount = (await em.repo(items).count()).data.count;
|
const oldCount = (await em.repo(items).count()).count;
|
||||||
const inserts = [{ label: "insert 1" }, { label: "insert 2" }];
|
const inserts = [{ label: "insert 1" }, { label: "insert 2" }];
|
||||||
const { data } = await em.mutator(items).insertMany(inserts);
|
const { data } = await em.mutator(items).insertMany(inserts);
|
||||||
|
|
||||||
expect(data.length).toBe(2);
|
expect(data.length).toBe(2);
|
||||||
expect(data.map((d) => ({ label: d.label }))).toEqual(inserts);
|
expect(data.map((d) => ({ label: d.label }))).toEqual(inserts);
|
||||||
const newCount = (await em.repo(items).count()).data.count;
|
const newCount = (await em.repo(items).count()).count;
|
||||||
expect(newCount).toBe(oldCount + inserts.length);
|
expect(newCount).toBe(oldCount + inserts.length);
|
||||||
|
|
||||||
const { data: data2 } = await em.repo(items).findMany({ offset: oldCount });
|
const { data: data2 } = await em.repo(items).findMany({ offset: oldCount });
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { afterAll, expect as bunExpect, describe, test } from "bun:test";
|
import { afterAll, expect as bunExpect, describe, test } from "bun:test";
|
||||||
import { stripMark } from "core/utils/schema";
|
import { stripMark } from "../../src/core/utils";
|
||||||
import { Entity, EntityManager } from "data/entities";
|
import { Entity, EntityManager, PolymorphicRelation, TextField } from "../../src/data";
|
||||||
import { TextField } from "data/fields";
|
|
||||||
import { PolymorphicRelation } from "data/relations";
|
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
|
|||||||
@@ -2,20 +2,19 @@ import { describe, expect, test } from "bun:test";
|
|||||||
import {
|
import {
|
||||||
BooleanField,
|
BooleanField,
|
||||||
DateField,
|
DateField,
|
||||||
|
Entity,
|
||||||
|
EntityIndex,
|
||||||
|
EntityManager,
|
||||||
EnumField,
|
EnumField,
|
||||||
JsonField,
|
JsonField,
|
||||||
NumberField,
|
|
||||||
TextField,
|
|
||||||
EntityIndex,
|
|
||||||
} from "data/fields";
|
|
||||||
import { Entity, EntityManager } from "data/entities";
|
|
||||||
import {
|
|
||||||
ManyToManyRelation,
|
ManyToManyRelation,
|
||||||
ManyToOneRelation,
|
ManyToOneRelation,
|
||||||
|
NumberField,
|
||||||
OneToOneRelation,
|
OneToOneRelation,
|
||||||
PolymorphicRelation,
|
PolymorphicRelation,
|
||||||
} from "data/relations";
|
TextField,
|
||||||
import { DummyConnection } from "data/connection/DummyConnection";
|
} from "../../src/data";
|
||||||
|
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
||||||
import {
|
import {
|
||||||
FieldPrototype,
|
FieldPrototype,
|
||||||
type FieldSchema,
|
type FieldSchema,
|
||||||
@@ -33,8 +32,8 @@ import {
|
|||||||
number,
|
number,
|
||||||
relation,
|
relation,
|
||||||
text,
|
text,
|
||||||
} from "data/prototype";
|
} from "../../src/data/prototype";
|
||||||
import { MediaField } from "media/MediaField";
|
import { MediaField } from "../../src/media/MediaField";
|
||||||
|
|
||||||
describe("prototype", () => {
|
describe("prototype", () => {
|
||||||
test("...", () => {
|
test("...", () => {
|
||||||
@@ -102,8 +101,7 @@ describe("prototype", () => {
|
|||||||
|
|
||||||
type Posts = Schema<typeof posts2>;
|
type Posts = Schema<typeof posts2>;
|
||||||
|
|
||||||
// @todo: check
|
expect(posts1.toJSON()).toEqual(posts2.toJSON());
|
||||||
//expect(posts1.toJSON()).toEqual(posts2.toJSON());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("test example", async () => {
|
test("test example", async () => {
|
||||||
@@ -297,9 +295,9 @@ describe("prototype", () => {
|
|||||||
new Entity("posts", [new TextField("name"), new TextField("slug", { required: true })]),
|
new Entity("posts", [new TextField("name"), new TextField("slug", { required: true })]),
|
||||||
new Entity("comments", [new TextField("some")]),
|
new Entity("comments", [new TextField("some")]),
|
||||||
new Entity("users", [new TextField("email")]),
|
new Entity("users", [new TextField("email")]),
|
||||||
] as const;
|
];
|
||||||
const _em2 = new EntityManager(
|
const _em2 = new EntityManager(
|
||||||
[...es],
|
es,
|
||||||
new DummyConnection(),
|
new DummyConnection(),
|
||||||
[new ManyToOneRelation(es[0], es[1]), new ManyToOneRelation(es[0], es[2])],
|
[new ManyToOneRelation(es[0], es[1]), new ManyToOneRelation(es[0], es[2])],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
// eslint-disable-next-line import/no-unresolved
|
// eslint-disable-next-line import/no-unresolved
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { Entity, EntityManager } from "data/entities";
|
import { Entity, EntityManager, TextField } from "../../src/data";
|
||||||
import { TextField } from "data/fields";
|
|
||||||
import {
|
import {
|
||||||
ManyToManyRelation,
|
ManyToManyRelation,
|
||||||
ManyToOneRelation,
|
ManyToOneRelation,
|
||||||
OneToOneRelation,
|
OneToOneRelation,
|
||||||
PolymorphicRelation,
|
PolymorphicRelation,
|
||||||
RelationField,
|
RelationField,
|
||||||
} from "data/relations";
|
} from "../../src/data/relations";
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
@@ -78,7 +77,7 @@ describe("Relations", async () => {
|
|||||||
const em = new EntityManager(entities, dummyConnection, relations);
|
const em = new EntityManager(entities, dummyConnection, relations);
|
||||||
|
|
||||||
// verify naming
|
// verify naming
|
||||||
const rel = em.relations.all[0]!;
|
const rel = em.relations.all[0];
|
||||||
expect(rel.source.entity.name).toBe(posts.name);
|
expect(rel.source.entity.name).toBe(posts.name);
|
||||||
expect(rel.source.reference).toBe(posts.name);
|
expect(rel.source.reference).toBe(posts.name);
|
||||||
expect(rel.target.entity.name).toBe(users.name);
|
expect(rel.target.entity.name).toBe(users.name);
|
||||||
@@ -90,11 +89,11 @@ describe("Relations", async () => {
|
|||||||
// verify low level relation
|
// verify low level relation
|
||||||
expect(em.relationsOf(users.name).length).toBe(1);
|
expect(em.relationsOf(users.name).length).toBe(1);
|
||||||
expect(em.relationsOf(users.name).length).toBe(1);
|
expect(em.relationsOf(users.name).length).toBe(1);
|
||||||
expect(em.relationsOf(users.name)[0]!.source.entity).toBe(posts);
|
expect(em.relationsOf(users.name)[0].source.entity).toBe(posts);
|
||||||
expect(posts.field("author_id")).toBeInstanceOf(RelationField);
|
expect(posts.field("author_id")).toBeInstanceOf(RelationField);
|
||||||
expect(em.relationsOf(users.name).length).toBe(1);
|
expect(em.relationsOf(users.name).length).toBe(1);
|
||||||
expect(em.relationsOf(users.name).length).toBe(1);
|
expect(em.relationsOf(users.name).length).toBe(1);
|
||||||
expect(em.relationsOf(users.name)[0]!.source.entity).toBe(posts);
|
expect(em.relationsOf(users.name)[0].source.entity).toBe(posts);
|
||||||
|
|
||||||
// verify high level relation (from users)
|
// verify high level relation (from users)
|
||||||
const userPostsRel = em.relationOf(users.name, "posts");
|
const userPostsRel = em.relationOf(users.name, "posts");
|
||||||
@@ -192,7 +191,7 @@ describe("Relations", async () => {
|
|||||||
const em = new EntityManager(entities, dummyConnection, relations);
|
const em = new EntityManager(entities, dummyConnection, relations);
|
||||||
|
|
||||||
// verify naming
|
// verify naming
|
||||||
const rel = em.relations.all[0]!;
|
const rel = em.relations.all[0];
|
||||||
expect(rel.source.entity.name).toBe(users.name);
|
expect(rel.source.entity.name).toBe(users.name);
|
||||||
expect(rel.source.reference).toBe(users.name);
|
expect(rel.source.reference).toBe(users.name);
|
||||||
expect(rel.target.entity.name).toBe(settings.name);
|
expect(rel.target.entity.name).toBe(settings.name);
|
||||||
@@ -203,8 +202,8 @@ describe("Relations", async () => {
|
|||||||
|
|
||||||
expect(em.relationsOf(users.name).length).toBe(1);
|
expect(em.relationsOf(users.name).length).toBe(1);
|
||||||
expect(em.relationsOf(users.name).length).toBe(1);
|
expect(em.relationsOf(users.name).length).toBe(1);
|
||||||
expect(em.relationsOf(users.name)[0]!.source.entity).toBe(users);
|
expect(em.relationsOf(users.name)[0].source.entity).toBe(users);
|
||||||
expect(em.relationsOf(users.name)[0]!.target.entity).toBe(settings);
|
expect(em.relationsOf(users.name)[0].target.entity).toBe(settings);
|
||||||
|
|
||||||
// verify high level relation (from users)
|
// verify high level relation (from users)
|
||||||
const userSettingRel = em.relationOf(users.name, settings.name);
|
const userSettingRel = em.relationOf(users.name, settings.name);
|
||||||
@@ -324,7 +323,7 @@ describe("Relations", async () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// mutation info
|
// mutation info
|
||||||
expect(relations[0]!.helper(posts.name)!.getMutationInfo()).toEqual({
|
expect(relations[0].helper(posts.name)!.getMutationInfo()).toEqual({
|
||||||
reference: "categories",
|
reference: "categories",
|
||||||
local_field: undefined,
|
local_field: undefined,
|
||||||
$set: false,
|
$set: false,
|
||||||
@@ -335,7 +334,7 @@ describe("Relations", async () => {
|
|||||||
cardinality: undefined,
|
cardinality: undefined,
|
||||||
relation_type: "m:n",
|
relation_type: "m:n",
|
||||||
});
|
});
|
||||||
expect(relations[0]!.helper(categories.name)!.getMutationInfo()).toEqual({
|
expect(relations[0].helper(categories.name)!.getMutationInfo()).toEqual({
|
||||||
reference: "posts",
|
reference: "posts",
|
||||||
local_field: undefined,
|
local_field: undefined,
|
||||||
$set: false,
|
$set: false,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Entity } from "data/entities";
|
import { Entity, NumberField, TextField } from "data";
|
||||||
import { NumberField, TextField } from "data/fields";
|
import * as p from "data/prototype";
|
||||||
|
|
||||||
describe("[data] Entity", async () => {
|
describe("[data] Entity", async () => {
|
||||||
const entity = new Entity("test", [
|
const entity = new Entity("test", [
|
||||||
@@ -47,4 +47,8 @@ describe("[data] Entity", async () => {
|
|||||||
entity.addField(field);
|
entity.addField(field);
|
||||||
expect(entity.getField("new_field")).toBe(field);
|
expect(entity.getField("new_field")).toBe(field);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.only("types", async () => {
|
||||||
|
console.log(entity.toTypes());
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { Entity, EntityManager } from "data/entities";
|
import {
|
||||||
import { ManyToManyRelation, ManyToOneRelation } from "data/relations";
|
Entity,
|
||||||
import { SchemaManager } from "data/schema/SchemaManager";
|
EntityManager,
|
||||||
import { UnableToConnectException } from "data/errors";
|
ManyToManyRelation,
|
||||||
|
ManyToOneRelation,
|
||||||
|
SchemaManager,
|
||||||
|
} from "../../../src/data";
|
||||||
|
import { UnableToConnectException } from "../../../src/data/errors";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { Entity, EntityManager } from "data/entities";
|
import { Entity, EntityManager, ManyToOneRelation, TextField } from "../../../src/data";
|
||||||
import { ManyToOneRelation } from "data/relations";
|
import { JoinBuilder } from "../../../src/data/entities/query/JoinBuilder";
|
||||||
import { TextField } from "data/fields";
|
|
||||||
import { JoinBuilder } from "data/entities/query/JoinBuilder";
|
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
afterAll(afterAllCleanup);
|
||||||
|
|
||||||
describe("[data] JoinBuilder", async () => {
|
describe("[data] JoinBuilder", async () => {
|
||||||
test("missing relation", async () => {
|
test("missing relation", async () => {
|
||||||
|
|||||||
@@ -1,24 +1,22 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import type { EventManager } from "../../../src/core/events";
|
import type { EventManager } from "../../../src/core/events";
|
||||||
import { Entity, EntityManager } from "data/entities";
|
|
||||||
import {
|
import {
|
||||||
|
Entity,
|
||||||
|
EntityManager,
|
||||||
ManyToOneRelation,
|
ManyToOneRelation,
|
||||||
|
MutatorEvents,
|
||||||
|
NumberField,
|
||||||
OneToOneRelation,
|
OneToOneRelation,
|
||||||
RelationField,
|
type RelationField,
|
||||||
RelationMutator,
|
RelationMutator,
|
||||||
} from "data/relations";
|
TextField,
|
||||||
import { NumberField, TextField } from "data/fields";
|
} from "../../../src/data";
|
||||||
import * as proto from "data/prototype";
|
import * as proto from "../../../src/data/prototype";
|
||||||
import { getDummyConnection } from "../../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
import { MutatorEvents } from "data/events";
|
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
afterAll(afterAllCleanup);
|
afterAll(afterAllCleanup);
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
|
||||||
|
|
||||||
describe("[data] Mutator (base)", async () => {
|
describe("[data] Mutator (base)", async () => {
|
||||||
const entity = new Entity("items", [
|
const entity = new Entity("items", [
|
||||||
new TextField("label", { required: true }),
|
new TextField("label", { required: true }),
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import type { Kysely, Transaction } from "kysely";
|
import type { Kysely, Transaction } from "kysely";
|
||||||
import { TextField } from "data/fields";
|
import { Perf } from "core/utils";
|
||||||
import { em as $em, entity as $entity, text as $text } from "data/prototype";
|
import {
|
||||||
import { Entity, EntityManager } from "data/entities";
|
Entity,
|
||||||
import { ManyToOneRelation } from "data/relations";
|
EntityManager,
|
||||||
import { RepositoryEvents } from "data/events";
|
LibsqlConnection,
|
||||||
|
ManyToOneRelation,
|
||||||
|
RepositoryEvents,
|
||||||
|
TextField,
|
||||||
|
entity as $entity,
|
||||||
|
text as $text,
|
||||||
|
em as $em,
|
||||||
|
} from "data";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
type E = Kysely<any> | Transaction<any>;
|
type E = Kysely<any> | Transaction<any>;
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
beforeAll(() => disableConsoleLog());
|
afterAll(afterAllCleanup);
|
||||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
|
||||||
|
|
||||||
async function sleep(ms: number) {
|
async function sleep(ms: number) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@@ -21,6 +26,120 @@ async function sleep(ms: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("[Repository]", async () => {
|
describe("[Repository]", async () => {
|
||||||
|
test.skip("bulk", async () => {
|
||||||
|
//const connection = dummyConnection;
|
||||||
|
//const connection = getLocalLibsqlConnection();
|
||||||
|
const credentials = null as any; // @todo: determine what to do here
|
||||||
|
const connection = new LibsqlConnection(credentials);
|
||||||
|
|
||||||
|
const em = new EntityManager([], connection);
|
||||||
|
/*const emLibsql = new EntityManager([], {
|
||||||
|
url: connection.url.replace("https", "libsql"),
|
||||||
|
authToken: connection.authToken,
|
||||||
|
});*/
|
||||||
|
const table = "posts";
|
||||||
|
|
||||||
|
const client = connection.getClient();
|
||||||
|
if (!client) {
|
||||||
|
console.log("Cannot perform test without libsql connection");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const conn = em.connection.kysely;
|
||||||
|
const selectQ = (e: E) => e.selectFrom(table).selectAll().limit(2);
|
||||||
|
const countQ = (e: E) => e.selectFrom(table).select(e.fn.count("*").as("count"));
|
||||||
|
|
||||||
|
async function executeTransaction(em: EntityManager<any>) {
|
||||||
|
return await em.connection.kysely.transaction().execute(async (e) => {
|
||||||
|
const res = await selectQ(e).execute();
|
||||||
|
const count = await countQ(e).execute();
|
||||||
|
|
||||||
|
return [res, count];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeBatch(em: EntityManager<any>) {
|
||||||
|
const queries = [selectQ(conn), countQ(conn)];
|
||||||
|
return await em.connection.batchQuery(queries);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeSingleKysely(em: EntityManager<any>) {
|
||||||
|
const res = await selectQ(conn).execute();
|
||||||
|
const count = await countQ(conn).execute();
|
||||||
|
return [res, count];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeSingleClient(em: EntityManager<any>) {
|
||||||
|
const q1 = selectQ(conn).compile();
|
||||||
|
const res = await client.execute({
|
||||||
|
sql: q1.sql,
|
||||||
|
args: q1.parameters as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
const q2 = countQ(conn).compile();
|
||||||
|
const count = await client.execute({
|
||||||
|
sql: q2.sql,
|
||||||
|
args: q2.parameters as any,
|
||||||
|
});
|
||||||
|
return [res, count];
|
||||||
|
}
|
||||||
|
|
||||||
|
const transaction = await executeTransaction(em);
|
||||||
|
const batch = await executeBatch(em);
|
||||||
|
|
||||||
|
expect(batch).toEqual(transaction as any);
|
||||||
|
|
||||||
|
const testperf = false;
|
||||||
|
if (testperf) {
|
||||||
|
const times = 5;
|
||||||
|
|
||||||
|
const exec = async (
|
||||||
|
name: string,
|
||||||
|
fn: (em: EntityManager<any>) => Promise<any>,
|
||||||
|
em: EntityManager<any>,
|
||||||
|
) => {
|
||||||
|
const res = await Perf.execute(() => fn(em), times);
|
||||||
|
await sleep(1000);
|
||||||
|
const info = {
|
||||||
|
name,
|
||||||
|
total: res.total.toFixed(2),
|
||||||
|
avg: (res.total / times).toFixed(2),
|
||||||
|
first: res.marks[0].time.toFixed(2),
|
||||||
|
last: res.marks[res.marks.length - 1].time.toFixed(2),
|
||||||
|
};
|
||||||
|
console.log(info.name, info, res.marks);
|
||||||
|
return info;
|
||||||
|
};
|
||||||
|
|
||||||
|
const data: any[] = [];
|
||||||
|
data.push(await exec("transaction.http", executeTransaction, em));
|
||||||
|
data.push(await exec("bulk.http", executeBatch, em));
|
||||||
|
data.push(await exec("singleKy.http", executeSingleKysely, em));
|
||||||
|
data.push(await exec("singleCl.http", executeSingleClient, em));
|
||||||
|
|
||||||
|
/*data.push(await exec("transaction.libsql", executeTransaction, emLibsql));
|
||||||
|
data.push(await exec("bulk.libsql", executeBatch, emLibsql));
|
||||||
|
data.push(await exec("singleKy.libsql", executeSingleKysely, emLibsql));
|
||||||
|
data.push(await exec("singleCl.libsql", executeSingleClient, emLibsql));*/
|
||||||
|
|
||||||
|
console.table(data);
|
||||||
|
/**
|
||||||
|
* ┌───┬────────────────────┬────────┬────────┬────────┬────────┐
|
||||||
|
* │ │ name │ total │ avg │ first │ last │
|
||||||
|
* ├───┼────────────────────┼────────┼────────┼────────┼────────┤
|
||||||
|
* │ 0 │ transaction.http │ 681.29 │ 136.26 │ 136.46 │ 396.09 │
|
||||||
|
* │ 1 │ bulk.http │ 164.82 │ 32.96 │ 32.95 │ 99.91 │
|
||||||
|
* │ 2 │ singleKy.http │ 330.01 │ 66.00 │ 65.86 │ 195.41 │
|
||||||
|
* │ 3 │ singleCl.http │ 326.17 │ 65.23 │ 61.32 │ 198.08 │
|
||||||
|
* │ 4 │ transaction.libsql │ 856.79 │ 171.36 │ 132.31 │ 595.24 │
|
||||||
|
* │ 5 │ bulk.libsql │ 180.63 │ 36.13 │ 35.39 │ 107.71 │
|
||||||
|
* │ 6 │ singleKy.libsql │ 347.11 │ 69.42 │ 65.00 │ 207.14 │
|
||||||
|
* │ 7 │ singleCl.libsql │ 328.60 │ 65.72 │ 62.19 │ 195.04 │
|
||||||
|
* └───┴────────────────────┴────────┴────────┴────────┴────────┘
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("count & exists", async () => {
|
test("count & exists", async () => {
|
||||||
const items = new Entity("items", [new TextField("label")]);
|
const items = new Entity("items", [new TextField("label")]);
|
||||||
const em = new EntityManager([items], dummyConnection);
|
const em = new EntityManager([items], dummyConnection);
|
||||||
@@ -41,44 +160,25 @@ describe("[Repository]", async () => {
|
|||||||
// count all
|
// count all
|
||||||
const res = await em.repository(items).count();
|
const res = await em.repository(items).count();
|
||||||
expect(res.sql).toBe('select count(*) as "count" from "items"');
|
expect(res.sql).toBe('select count(*) as "count" from "items"');
|
||||||
expect(res.data.count).toBe(3);
|
expect(res.count).toBe(3);
|
||||||
|
|
||||||
//
|
|
||||||
{
|
|
||||||
const res = await em.repository(items).findMany();
|
|
||||||
expect(res.count).toBe(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const res = await em
|
|
||||||
.repository(items, {
|
|
||||||
includeCounts: true,
|
|
||||||
})
|
|
||||||
.findMany();
|
|
||||||
expect(res.count).toBe(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
// count filtered
|
// count filtered
|
||||||
const res2 = await em
|
const res2 = await em.repository(items).count({ label: { $in: ["a", "b"] } });
|
||||||
.repository(items, {
|
|
||||||
includeCounts: true,
|
|
||||||
})
|
|
||||||
.count({ label: { $in: ["a", "b"] } });
|
|
||||||
|
|
||||||
expect(res2.sql).toBe('select count(*) as "count" from "items" where "label" in (?, ?)');
|
expect(res2.sql).toBe('select count(*) as "count" from "items" where "label" in (?, ?)');
|
||||||
expect(res2.parameters).toEqual(["a", "b"]);
|
expect(res2.parameters).toEqual(["a", "b"]);
|
||||||
expect(res2.data.count).toBe(2);
|
expect(res2.count).toBe(2);
|
||||||
|
|
||||||
// check exists
|
// check exists
|
||||||
const res3 = await em.repository(items).exists({ label: "a" });
|
const res3 = await em.repository(items).exists({ label: "a" });
|
||||||
expect(res3.data.exists).toBe(true);
|
expect(res3.exists).toBe(true);
|
||||||
|
|
||||||
const res4 = await em.repository(items).exists({ label: "d" });
|
const res4 = await em.repository(items).exists({ label: "d" });
|
||||||
expect(res4.data.exists).toBe(false);
|
expect(res4.exists).toBe(false);
|
||||||
|
|
||||||
// for now, allow empty filter
|
// for now, allow empty filter
|
||||||
const res5 = await em.repository(items).exists({});
|
const res5 = await em.repository(items).exists({});
|
||||||
expect(res5.data.exists).toBe(true);
|
expect(res5.exists).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("option: silent", async () => {
|
test("option: silent", async () => {
|
||||||
@@ -91,9 +191,6 @@ describe("[Repository]", async () => {
|
|||||||
// should throw because table doesn't exist
|
// should throw because table doesn't exist
|
||||||
expect(em.repo("items").findMany({})).rejects.toThrow(/no such table/);
|
expect(em.repo("items").findMany({})).rejects.toThrow(/no such table/);
|
||||||
// should silently return empty result
|
// should silently return empty result
|
||||||
em.repo("items", { silent: true })
|
|
||||||
.findMany({})
|
|
||||||
.then((r) => r.data);
|
|
||||||
expect(
|
expect(
|
||||||
em
|
em
|
||||||
.repo("items", { silent: true })
|
.repo("items", { silent: true })
|
||||||
@@ -112,16 +209,16 @@ describe("[Repository]", async () => {
|
|||||||
|
|
||||||
expect(
|
expect(
|
||||||
em
|
em
|
||||||
.repo("items", { includeCounts: true })
|
.repo("items")
|
||||||
.findMany({})
|
.findMany({})
|
||||||
.then((r) => [r.count, r.total]),
|
.then((r) => [r.meta.count, r.meta.total]),
|
||||||
).resolves.toEqual([0, 0]);
|
).resolves.toEqual([0, 0]);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
em
|
em
|
||||||
.repo("items", { includeCounts: false })
|
.repo("items", { includeCounts: false })
|
||||||
.findMany({})
|
.findMany({})
|
||||||
.then((r) => [r.count, r.total]),
|
.then((r) => [r.meta.count, r.meta.total]),
|
||||||
).resolves.toEqual([undefined, undefined]);
|
).resolves.toEqual([undefined, undefined]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
// eslint-disable-next-line import/no-unresolved
|
// eslint-disable-next-line import/no-unresolved
|
||||||
import { afterAll, describe, expect, spyOn, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { randomString } from "core/utils";
|
import { randomString } from "../../../src/core/utils";
|
||||||
import { Entity, EntityManager } from "data/entities";
|
import { Entity, EntityIndex, EntityManager, SchemaManager, TextField } from "../../../src/data";
|
||||||
import { TextField, EntityIndex } from "data/fields";
|
|
||||||
import { SchemaManager } from "data/schema/SchemaManager";
|
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
@@ -268,39 +266,4 @@ describe("SchemaManager tests", async () => {
|
|||||||
const diffAfter = await em.schema().getDiff();
|
const diffAfter = await em.schema().getDiff();
|
||||||
expect(diffAfter.length).toBe(0);
|
expect(diffAfter.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("returns statements", async () => {
|
|
||||||
const amount = 5;
|
|
||||||
const entities = new Array(amount)
|
|
||||||
.fill(0)
|
|
||||||
.map(() => new Entity(randomString(16), [new TextField("text")]));
|
|
||||||
const em = new EntityManager(entities, dummyConnection);
|
|
||||||
const statements = await em.schema().sync({ force: true });
|
|
||||||
expect(statements.length).toBe(amount);
|
|
||||||
expect(statements.every((stmt) => Object.keys(stmt).join(",") === "sql,parameters")).toBe(
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("batches statements", async () => {
|
|
||||||
const { dummyConnection } = getDummyConnection();
|
|
||||||
const entities = new Array(20)
|
|
||||||
.fill(0)
|
|
||||||
.map(() => new Entity(randomString(16), [new TextField("text")]));
|
|
||||||
const em = new EntityManager(entities, dummyConnection);
|
|
||||||
const spy = spyOn(em.connection, "executeQueries");
|
|
||||||
const statements = await em.schema().sync();
|
|
||||||
expect(statements.length).toBe(entities.length);
|
|
||||||
expect(statements.every((stmt) => Object.keys(stmt).join(",") === "sql,parameters")).toBe(
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
await em.schema().sync({ force: true });
|
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
|
||||||
const tables = await em.connection.kysely
|
|
||||||
.selectFrom("sqlite_master")
|
|
||||||
.where("type", "=", "table")
|
|
||||||
.selectAll()
|
|
||||||
.execute();
|
|
||||||
expect(tables.length).toBe(entities.length + 1); /* 1+ for sqlite_sequence */
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, test, expect } from "bun:test";
|
import { describe, test, expect } from "bun:test";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
import { WhereBuilder, type WhereQuery } from "data/entities/query/WhereBuilder";
|
import { type WhereQuery, WhereBuilder } from "data";
|
||||||
|
|
||||||
function qb() {
|
function qb() {
|
||||||
const c = getDummyConnection();
|
const c = getDummyConnection();
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Entity, EntityManager } from "data/entities";
|
import {
|
||||||
import { ManyToManyRelation, ManyToOneRelation, PolymorphicRelation } from "data/relations";
|
Entity,
|
||||||
import { TextField } from "data/fields";
|
EntityManager,
|
||||||
import * as proto from "data/prototype";
|
ManyToManyRelation,
|
||||||
import { WithBuilder } from "data/entities/query/WithBuilder";
|
ManyToOneRelation,
|
||||||
|
PolymorphicRelation,
|
||||||
|
TextField,
|
||||||
|
WithBuilder,
|
||||||
|
} from "../../../src/data";
|
||||||
|
import * as proto from "../../../src/data/prototype";
|
||||||
import { schemaToEm } from "../../helper";
|
import { schemaToEm } from "../../helper";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
|
|
||||||
@@ -88,9 +89,9 @@ describe("[data] WithBuilder", async () => {
|
|||||||
const res2 = qb2.compile();
|
const res2 = qb2.compile();
|
||||||
|
|
||||||
expect(res2.sql).toBe(
|
expect(res2.sql).toBe(
|
||||||
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "users"."id" as "id", "users"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" order by "users"."id" asc limit ?) as obj) as "author" from "posts"',
|
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "users"."id" as "id", "users"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" order by "users"."id" asc limit ? offset ?) as obj) as "author" from "posts"',
|
||||||
);
|
);
|
||||||
expect(res2.parameters).toEqual([1]);
|
expect(res2.parameters).toEqual([1, 0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("test with empty join", async () => {
|
test("test with empty join", async () => {
|
||||||
@@ -193,9 +194,9 @@ describe("[data] WithBuilder", async () => {
|
|||||||
);
|
);
|
||||||
const res = qb.compile();
|
const res = qb.compile();
|
||||||
expect(res.sql).toBe(
|
expect(res.sql).toBe(
|
||||||
'select (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ?) as obj) as "single" from "categories"',
|
'select (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as obj) as "single" from "categories"',
|
||||||
);
|
);
|
||||||
expect(res.parameters).toEqual(["categories.single", 1]);
|
expect(res.parameters).toEqual(["categories.single", 1, 0]);
|
||||||
|
|
||||||
const qb2 = WithBuilder.addClause(
|
const qb2 = WithBuilder.addClause(
|
||||||
em,
|
em,
|
||||||
@@ -272,9 +273,9 @@ describe("[data] WithBuilder", async () => {
|
|||||||
|
|
||||||
//prettyPrintQb(qb);
|
//prettyPrintQb(qb);
|
||||||
expect(qb.compile().sql).toBe(
|
expect(qb.compile().sql).toBe(
|
||||||
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username", \'avatar\', "obj"."avatar") from (select "users"."id" as "id", "users"."username" as "username", (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "users"."id" = "media"."entity_id" order by "media"."id" asc limit ?) as obj) as "avatar" from "users" as "users" where "users"."id" = "posts"."users_id" order by "users"."username" asc limit ?) as obj) as "users" from "posts"',
|
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username", \'avatar\', "obj"."avatar") from (select "users"."id" as "id", "users"."username" as "username", (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "users"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as obj) as "avatar" from "users" as "users" where "users"."id" = "posts"."users_id" order by "users"."username" asc limit ? offset ?) as obj) as "users" from "posts"',
|
||||||
);
|
);
|
||||||
expect(qb.compile().parameters).toEqual(["users.avatar", 1, 1]);
|
expect(qb.compile().parameters).toEqual(["users.avatar", 1, 0, 1, 0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("compiles with many", async () => {
|
test("compiles with many", async () => {
|
||||||
@@ -314,9 +315,9 @@ describe("[data] WithBuilder", async () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(qb.compile().sql).toBe(
|
expect(qb.compile().sql).toBe(
|
||||||
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'posts_id\', "agg"."posts_id", \'users_id\', "agg"."users_id", \'users\', "agg"."users")), \'[]\') from (select "comments"."id" as "id", "comments"."posts_id" as "posts_id", "comments"."users_id" as "users_id", (select json_object(\'username\', "obj"."username") from (select "users"."username" as "username" from "users" as "users" where "users"."id" = "comments"."users_id" order by "users"."id" asc limit ?) as obj) as "users" from "comments" as "comments" where "comments"."posts_id" = "posts"."id" order by "comments"."id" asc limit ? offset ?) as agg) as "comments" from "posts"',
|
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'posts_id\', "agg"."posts_id", \'users_id\', "agg"."users_id", \'users\', "agg"."users")), \'[]\') from (select "comments"."id" as "id", "comments"."posts_id" as "posts_id", "comments"."users_id" as "users_id", (select json_object(\'username\', "obj"."username") from (select "users"."username" as "username" from "users" as "users" where "users"."id" = "comments"."users_id" order by "users"."id" asc limit ? offset ?) as obj) as "users" from "comments" as "comments" where "comments"."posts_id" = "posts"."id" order by "comments"."id" asc limit ? offset ?) as agg) as "comments" from "posts"',
|
||||||
);
|
);
|
||||||
expect(qb.compile().parameters).toEqual([1, 12, 0]);
|
expect(qb.compile().parameters).toEqual([1, 0, 12, 0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("returns correct result", async () => {
|
test("returns correct result", async () => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { EntityManager } from "data/entities/EntityManager";
|
import { EntityManager } from "../../../../src/data";
|
||||||
import { getDummyConnection } from "../../helper";
|
import { getDummyConnection } from "../../helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { bunTestRunner } from "adapter/bun/test";
|
|
||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { BooleanField } from "data/fields";
|
import { BooleanField } from "../../../../src/data";
|
||||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
describe("[data] BooleanField", async () => {
|
describe("[data] BooleanField", async () => {
|
||||||
fieldTestSuite(bunTestRunner, BooleanField, { defaultValue: true, schemaType: "boolean" });
|
fieldTestSuite({ expect, test }, BooleanField, { defaultValue: true, schemaType: "boolean" });
|
||||||
|
|
||||||
test("transformRetrieve", async () => {
|
test("transformRetrieve", async () => {
|
||||||
const field = new BooleanField("test");
|
const field = new BooleanField("test");
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
import { describe, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { DateField } from "data/fields";
|
import { DateField } from "../../../../src/data";
|
||||||
import { fieldTestSuite } from "data/fields/field-test-suite";
|
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||||
import { bunTestRunner } from "adapter/bun/test";
|
|
||||||
|
|
||||||
describe("[data] DateField", async () => {
|
describe("[data] DateField", async () => {
|
||||||
fieldTestSuite(
|
fieldTestSuite({ expect, test }, DateField, { defaultValue: new Date(), schemaType: "date" });
|
||||||
bunTestRunner,
|
|
||||||
DateField,
|
|
||||||
{ defaultValue: new Date(), schemaType: "date" },
|
|
||||||
{ type: "date" },
|
|
||||||
);
|
|
||||||
|
|
||||||
// @todo: add datefield tests
|
// @todo: add datefield tests
|
||||||
test("week", async () => {
|
test("week", async () => {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { bunTestRunner } from "adapter/bun/test";
|
|
||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { EnumField } from "data/fields";
|
import { EnumField } from "../../../../src/data";
|
||||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
function options(strings: string[]) {
|
function options(strings: string[]) {
|
||||||
@@ -9,7 +8,7 @@ function options(strings: string[]) {
|
|||||||
|
|
||||||
describe("[data] EnumField", async () => {
|
describe("[data] EnumField", async () => {
|
||||||
fieldTestSuite(
|
fieldTestSuite(
|
||||||
bunTestRunner,
|
{ expect, test },
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
EnumField,
|
EnumField,
|
||||||
{ defaultValue: "a", schemaType: "text" },
|
{ defaultValue: "a", schemaType: "text" },
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { Default, stripMark } from "../../../../src/core/utils";
|
||||||
import { baseFieldConfigSchema, Field } from "../../../../src/data/fields/Field";
|
import { baseFieldConfigSchema, Field } from "../../../../src/data/fields/Field";
|
||||||
import { fieldTestSuite } from "data/fields/field-test-suite";
|
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||||
import { bunTestRunner } from "adapter/bun/test";
|
|
||||||
import { stripMark } from "core/utils/schema";
|
|
||||||
|
|
||||||
describe("[data] Field", async () => {
|
describe("[data] Field", async () => {
|
||||||
class FieldSpec extends Field {
|
class FieldSpec extends Field {
|
||||||
@@ -20,10 +19,10 @@ describe("[data] Field", async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
fieldTestSuite(bunTestRunner, FieldSpec, { defaultValue: "test", schemaType: "text" });
|
fieldTestSuite({ expect, test }, FieldSpec, { defaultValue: "test", schemaType: "text" });
|
||||||
|
|
||||||
test("default config", async () => {
|
test("default config", async () => {
|
||||||
const config = baseFieldConfigSchema.template({});
|
const config = Default(baseFieldConfigSchema, {});
|
||||||
expect(stripMark(new FieldSpec("test").config)).toEqual(config as any);
|
expect(stripMark(new FieldSpec("test").config)).toEqual(config as any);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Entity } from "data/entities";
|
import { Type } from "@sinclair/typebox";
|
||||||
import { Field, EntityIndex } from "data/fields";
|
import { Entity, EntityIndex, Field } from "../../../../src/data";
|
||||||
import { s } from "core/utils/schema";
|
|
||||||
|
|
||||||
class TestField extends Field {
|
class TestField extends Field {
|
||||||
protected getSchema(): any {
|
protected getSchema(): any {
|
||||||
return s.any();
|
return Type.Any();
|
||||||
}
|
}
|
||||||
|
|
||||||
override schema() {
|
override schema() {
|
||||||
@@ -23,4 +22,11 @@ describe("FieldIndex", async () => {
|
|||||||
expect(index.name).toEqual("idx_test_name");
|
expect(index.name).toEqual("idx_test_name");
|
||||||
expect(index.unique).toEqual(false);
|
expect(index.unique).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("it fails on non-unique", async () => {
|
||||||
|
const field = new TestField("name", { required: false });
|
||||||
|
|
||||||
|
expect(() => new EntityIndex(entity, [field], true)).toThrowError();
|
||||||
|
expect(() => new EntityIndex(entity, [field])).toBeDefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { bunTestRunner } from "adapter/bun/test";
|
|
||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { JsonField } from "data/fields";
|
import { JsonField } from "../../../../src/data";
|
||||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
describe("[data] JsonField", async () => {
|
describe("[data] JsonField", async () => {
|
||||||
const field = new JsonField("test");
|
const field = new JsonField("test");
|
||||||
fieldTestSuite(bunTestRunner, JsonField, {
|
fieldTestSuite({ expect, test }, JsonField, {
|
||||||
defaultValue: { a: 1 },
|
defaultValue: { a: 1 },
|
||||||
//sampleValues: ["string", { test: 1 }, 1],
|
sampleValues: ["string", { test: 1 }, 1],
|
||||||
schemaType: "text",
|
schemaType: "text",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -33,9 +32,9 @@ describe("[data] JsonField", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("getValue", async () => {
|
test("getValue", async () => {
|
||||||
expect(field.getValue({ test: 1 }, "form")).toEqual({ test: 1 });
|
expect(field.getValue({ test: 1 }, "form")).toBe('{\n "test": 1\n}');
|
||||||
expect(field.getValue("string", "form")).toBe("string");
|
expect(field.getValue("string", "form")).toBe('"string"');
|
||||||
expect(field.getValue(1, "form")).toBe(1);
|
expect(field.getValue(1, "form")).toBe("1");
|
||||||
|
|
||||||
expect(field.getValue('{"test":1}', "submit")).toEqual({ test: 1 });
|
expect(field.getValue('{"test":1}', "submit")).toEqual({ test: 1 });
|
||||||
expect(field.getValue('"string"', "submit")).toBe("string");
|
expect(field.getValue('"string"', "submit")).toBe("string");
|
||||||
@@ -43,5 +42,6 @@ describe("[data] JsonField", async () => {
|
|||||||
|
|
||||||
expect(field.getValue({ test: 1 }, "table")).toBe('{"test":1}');
|
expect(field.getValue({ test: 1 }, "table")).toBe('{"test":1}');
|
||||||
expect(field.getValue("string", "table")).toBe('"string"');
|
expect(field.getValue("string", "table")).toBe('"string"');
|
||||||
|
expect(field.getValue(1, "form")).toBe("1");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { JsonSchemaField } from "data/fields";
|
import { JsonSchemaField } from "../../../../src/data";
|
||||||
import { fieldTestSuite } from "data/fields/field-test-suite";
|
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
describe("[data] JsonSchemaField", async () => {
|
describe("[data] JsonSchemaField", async () => {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { bunTestRunner } from "adapter/bun/test";
|
|
||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { NumberField } from "data/fields";
|
import { NumberField } from "../../../../src/data";
|
||||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
describe("[data] NumberField", async () => {
|
describe("[data] NumberField", async () => {
|
||||||
@@ -16,5 +15,5 @@ describe("[data] NumberField", async () => {
|
|||||||
expect(transformPersist(field2, 10000)).resolves.toBe(10000);
|
expect(transformPersist(field2, 10000)).resolves.toBe(10000);
|
||||||
});
|
});
|
||||||
|
|
||||||
fieldTestSuite(bunTestRunner, NumberField, { defaultValue: 12, schemaType: "integer" });
|
fieldTestSuite({ expect, test }, NumberField, { defaultValue: 12, schemaType: "integer" });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { PrimaryField } from "data/fields";
|
import { PrimaryField } from "../../../../src/data";
|
||||||
|
|
||||||
describe("[data] PrimaryField", async () => {
|
describe("[data] PrimaryField", async () => {
|
||||||
const field = new PrimaryField("primary");
|
const field = new PrimaryField("primary");
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { TextField } from "data/fields";
|
import { TextField } from "../../../../src/data";
|
||||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
import { bunTestRunner } from "adapter/bun/test";
|
|
||||||
|
|
||||||
describe("[data] TextField", async () => {
|
describe("[data] TextField", async () => {
|
||||||
test("transformPersist (config)", async () => {
|
test("transformPersist (config)", async () => {
|
||||||
@@ -12,5 +11,5 @@ describe("[data] TextField", async () => {
|
|||||||
expect(transformPersist(field, "abc")).resolves.toBe("abc");
|
expect(transformPersist(field, "abc")).resolves.toBe("abc");
|
||||||
});
|
});
|
||||||
|
|
||||||
fieldTestSuite(bunTestRunner, TextField, { defaultValue: "abc", schemaType: "text" });
|
fieldTestSuite({ expect, test }, TextField, { defaultValue: "abc", schemaType: "text" });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import { describe, expect, it, test } from "bun:test";
|
import { describe, expect, it, test } from "bun:test";
|
||||||
import { Entity, type EntityManager } from "data/entities";
|
import { Entity, type EntityManager } from "../../../../src/data";
|
||||||
import {
|
import {
|
||||||
type BaseRelationConfig,
|
type BaseRelationConfig,
|
||||||
EntityRelation,
|
EntityRelation,
|
||||||
EntityRelationAnchor,
|
EntityRelationAnchor,
|
||||||
ManyToManyRelation,
|
|
||||||
RelationTypes,
|
RelationTypes,
|
||||||
} from "data/relations";
|
} from "../../../../src/data/relations";
|
||||||
import * as proto from "data/prototype";
|
|
||||||
|
|
||||||
class TestEntityRelation extends EntityRelation {
|
class TestEntityRelation extends EntityRelation {
|
||||||
constructor(config?: BaseRelationConfig) {
|
constructor(config?: BaseRelationConfig) {
|
||||||
@@ -26,11 +24,11 @@ class TestEntityRelation extends EntityRelation {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildWith(): any {
|
buildWith(a: any, b: any, c: any): any {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildJoin(): any {
|
buildJoin(a: any, b: any): any {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,15 +75,4 @@ describe("[data] EntityRelation", async () => {
|
|||||||
const relation2 = new TestEntityRelation({ required: true });
|
const relation2 = new TestEntityRelation({ required: true });
|
||||||
expect(relation2.required).toBe(true);
|
expect(relation2.required).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("correctly produces the relation name", async () => {
|
|
||||||
const relation = new ManyToManyRelation(new Entity("apps"), new Entity("organizations"));
|
|
||||||
expect(relation.getName()).not.toContain(",");
|
|
||||||
expect(relation.getName()).toBe("mn_apps_organizations");
|
|
||||||
|
|
||||||
const relation2 = new ManyToManyRelation(new Entity("apps"), new Entity("organizations"), {
|
|
||||||
connectionTableMappedName: "appOrganizations",
|
|
||||||
});
|
|
||||||
expect(relation2.getName()).toBe("mn_apps_organizations_appOrganizations");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import { describe, it, expect } from "bun:test";
|
|
||||||
import * as sDirect from "jsonv-ts";
|
|
||||||
import { s as sFromBknd } from "bknd/utils";
|
|
||||||
|
|
||||||
describe("jsonv-ts resolution", () => {
|
|
||||||
it("should resolve to a single instance", () => {
|
|
||||||
const sameNamespace = sDirect === (sFromBknd as unknown as typeof sDirect);
|
|
||||||
// If this fails, two instances are being loaded via different specifiers/paths
|
|
||||||
expect(sameNamespace).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should resolve specifiers to a single package path", async () => {
|
|
||||||
const base = await import.meta.resolve("jsonv-ts");
|
|
||||||
const hono = await import.meta.resolve("jsonv-ts/hono");
|
|
||||||
const mcp = await import.meta.resolve("jsonv-ts/mcp");
|
|
||||||
expect(typeof base).toBe("string");
|
|
||||||
expect(typeof hono).toBe("string");
|
|
||||||
expect(typeof mcp).toBe("string");
|
|
||||||
// They can be different files (subpath exports), but they should share the same package root
|
|
||||||
const pkgRoot = (p: string) => p.slice(0, p.lastIndexOf("jsonv-ts") + "jsonv-ts".length);
|
|
||||||
expect(pkgRoot(base)).toBe(pkgRoot(hono));
|
|
||||||
expect(pkgRoot(base)).toBe(pkgRoot(mcp));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -41,7 +41,7 @@ beforeAll(() =>
|
|||||||
);
|
);
|
||||||
afterAll(unmockFetch);
|
afterAll(unmockFetch);
|
||||||
|
|
||||||
describe.skip("FetchTask", async () => {
|
describe("FetchTask", async () => {
|
||||||
test("Simple fetch", async () => {
|
test("Simple fetch", async () => {
|
||||||
const task = new FetchTask("Fetch Something", {
|
const task = new FetchTask("Fetch Something", {
|
||||||
url: "https://jsonplaceholder.typicode.com/todos/1",
|
url: "https://jsonplaceholder.typicode.com/todos/1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Flow, LogTask, SubFlowTask, RenderTask, Task } from "../../src/flows";
|
import { Flow, LogTask, SubFlowTask, RenderTask, Task } from "../../src/flows";
|
||||||
import { s } from "core/utils/schema";
|
import { Type } from "@sinclair/typebox";
|
||||||
|
|
||||||
export class StringifyTask<Output extends string> extends Task<
|
export class StringifyTask<Output extends string> extends Task<
|
||||||
typeof StringifyTask.schema,
|
typeof StringifyTask.schema,
|
||||||
@@ -8,16 +8,18 @@ export class StringifyTask<Output extends string> extends Task<
|
|||||||
> {
|
> {
|
||||||
type = "stringify";
|
type = "stringify";
|
||||||
|
|
||||||
static override schema = s.object({
|
static override schema = Type.Optional(
|
||||||
input: s.string().optional(),
|
Type.Object({
|
||||||
});
|
input: Type.Optional(Type.String()),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
async execute() {
|
async execute() {
|
||||||
return JSON.stringify(this.params.input) as Output;
|
return JSON.stringify(this.params.input) as Output;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe.skip("SubFlowTask", async () => {
|
describe("SubFlowTask", async () => {
|
||||||
test("Simple Subflow", async () => {
|
test("Simple Subflow", async () => {
|
||||||
const subTask = new RenderTask("render", {
|
const subTask = new RenderTask("render", {
|
||||||
render: "subflow",
|
render: "subflow",
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { Type } from "@sinclair/typebox";
|
||||||
import { Task } from "../../src/flows";
|
import { Task } from "../../src/flows";
|
||||||
import { dynamic } from "../../src/flows/tasks/Task";
|
import { dynamic } from "../../src/flows/tasks/Task";
|
||||||
import { s } from "core/utils/schema";
|
|
||||||
|
|
||||||
describe.skip("Task", async () => {
|
describe("Task", async () => {
|
||||||
test("resolveParams: template with parse", async () => {
|
test("resolveParams: template with parse", async () => {
|
||||||
const result = await Task.resolveParams(
|
const result = await Task.resolveParams(
|
||||||
s.object({ test: dynamic(s.number()) }),
|
Type.Object({ test: dynamic(Type.Number()) }),
|
||||||
{
|
{
|
||||||
test: "{{ some.path }}",
|
test: "{{ some.path }}",
|
||||||
},
|
},
|
||||||
@@ -22,7 +22,7 @@ describe.skip("Task", async () => {
|
|||||||
|
|
||||||
test("resolveParams: with string", async () => {
|
test("resolveParams: with string", async () => {
|
||||||
const result = await Task.resolveParams(
|
const result = await Task.resolveParams(
|
||||||
s.object({ test: s.string() }),
|
Type.Object({ test: Type.String() }),
|
||||||
{
|
{
|
||||||
test: "{{ some.path }}",
|
test: "{{ some.path }}",
|
||||||
},
|
},
|
||||||
@@ -38,7 +38,7 @@ describe.skip("Task", async () => {
|
|||||||
|
|
||||||
test("resolveParams: with object", async () => {
|
test("resolveParams: with object", async () => {
|
||||||
const result = await Task.resolveParams(
|
const result = await Task.resolveParams(
|
||||||
s.object({ test: dynamic(s.object({ key: s.string(), value: s.string() })) }),
|
Type.Object({ test: dynamic(Type.Object({ key: Type.String(), value: Type.String() })) }),
|
||||||
{
|
{
|
||||||
test: { key: "path", value: "{{ some.path }}" },
|
test: { key: "path", value: "{{ some.path }}" },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { Event, EventManager } from "../../src/core/events";
|
import { Event, EventManager } from "../../src/core/events";
|
||||||
import { s, parse } from "core/utils/schema";
|
import { parse } from "../../src/core/utils";
|
||||||
|
import { type Static, type StaticDecode, Type } from "@sinclair/typebox";
|
||||||
import { EventTrigger, Flow, HttpTrigger, type InputsMap, Task } from "../../src/flows";
|
import { EventTrigger, Flow, HttpTrigger, type InputsMap, Task } from "../../src/flows";
|
||||||
import { dynamic } from "../../src/flows/tasks/Task";
|
import { dynamic } from "../../src/flows/tasks/Task";
|
||||||
|
|
||||||
@@ -14,15 +15,15 @@ class Passthrough extends Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type OutputIn = s.Static<typeof OutputParamTask.schema>;
|
type OutputIn = Static<typeof OutputParamTask.schema>;
|
||||||
type OutputOut = s.StaticCoerced<typeof OutputParamTask.schema>;
|
type OutputOut = StaticDecode<typeof OutputParamTask.schema>;
|
||||||
|
|
||||||
class OutputParamTask extends Task<typeof OutputParamTask.schema> {
|
class OutputParamTask extends Task<typeof OutputParamTask.schema> {
|
||||||
type = "output-param";
|
type = "output-param";
|
||||||
|
|
||||||
static override schema = s.strictObject({
|
static override schema = Type.Object({
|
||||||
number: dynamic(
|
number: dynamic(
|
||||||
s.number({
|
Type.Number({
|
||||||
title: "Output number",
|
title: "Output number",
|
||||||
}),
|
}),
|
||||||
Number.parseInt,
|
Number.parseInt,
|
||||||
@@ -43,7 +44,7 @@ class PassthroughFlowInput extends Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe.skip("Flow task inputs", async () => {
|
describe("Flow task inputs", async () => {
|
||||||
test("types", async () => {
|
test("types", async () => {
|
||||||
const schema = OutputParamTask.schema;
|
const schema = OutputParamTask.schema;
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class ExecTask extends Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe.skip("Flow trigger", async () => {
|
describe("Flow trigger", async () => {
|
||||||
test("manual trigger", async () => {
|
test("manual trigger", async () => {
|
||||||
let called = false;
|
let called = false;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { isEqual } from "lodash-es";
|
import { isEqual } from "lodash-es";
|
||||||
import { _jsonp, withDisabledConsole } from "../../src/core/utils";
|
import { _jsonp, withDisabledConsole } from "../../src/core/utils";
|
||||||
import { s } from "core/utils/schema";
|
import { type Static, Type } from "@sinclair/typebox";
|
||||||
import { Condition, ExecutionEvent, FetchTask, Flow, LogTask, Task } from "../../src/flows";
|
import { Condition, ExecutionEvent, FetchTask, Flow, LogTask, Task } from "../../src/flows";
|
||||||
|
|
||||||
/*beforeAll(disableConsoleLog);
|
/*beforeAll(disableConsoleLog);
|
||||||
@@ -11,19 +11,19 @@ afterAll(enableConsoleLog);*/
|
|||||||
class ExecTask extends Task<typeof ExecTask.schema> {
|
class ExecTask extends Task<typeof ExecTask.schema> {
|
||||||
type = "exec";
|
type = "exec";
|
||||||
|
|
||||||
static override schema = s.object({
|
static override schema = Type.Object({
|
||||||
delay: s.number({ default: 10 }),
|
delay: Type.Number({ default: 10 }),
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
name: string,
|
name: string,
|
||||||
params: s.Static<typeof ExecTask.schema>,
|
params: Static<typeof ExecTask.schema>,
|
||||||
private func: () => Promise<any>,
|
private func: () => Promise<any>,
|
||||||
) {
|
) {
|
||||||
super(name, params);
|
super(name, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
override clone(name: string, params: s.Static<typeof ExecTask.schema>) {
|
override clone(name: string, params: Static<typeof ExecTask.schema>) {
|
||||||
return new ExecTask(name, params, this.func);
|
return new ExecTask(name, params, this.func);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ function getObjectDiff(obj1, obj2) {
|
|||||||
return diff;
|
return diff;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe.skip("Flow tests", async () => {
|
describe("Flow tests", async () => {
|
||||||
test("Simple single task", async () => {
|
test("Simple single task", async () => {
|
||||||
const simple = getTask(0);
|
const simple = getTask(0);
|
||||||
|
|
||||||
|
|||||||
+22
-4
@@ -2,12 +2,11 @@ import { unlink } from "node:fs/promises";
|
|||||||
import type { SelectQueryBuilder, SqliteDatabase } from "kysely";
|
import type { SelectQueryBuilder, SqliteDatabase } from "kysely";
|
||||||
import Database from "libsql";
|
import Database from "libsql";
|
||||||
import { format as sqlFormat } from "sql-formatter";
|
import { format as sqlFormat } from "sql-formatter";
|
||||||
|
import { type Connection, EntityManager, SqliteLocalConnection } from "../src/data";
|
||||||
import type { em as protoEm } from "../src/data/prototype";
|
import type { em as protoEm } from "../src/data/prototype";
|
||||||
import { writeFile } from "node:fs/promises";
|
import { writeFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { slugify } from "bknd/utils";
|
import { slugify } from "core/utils/strings";
|
||||||
import { type Connection, SqliteLocalConnection } from "data/connection";
|
|
||||||
import { EntityManager } from "data/entities/EntityManager";
|
|
||||||
|
|
||||||
export function getDummyDatabase(memory: boolean = true): {
|
export function getDummyDatabase(memory: boolean = true): {
|
||||||
dummyDb: SqliteDatabase;
|
dummyDb: SqliteDatabase;
|
||||||
@@ -39,6 +38,25 @@ export function getLocalLibsqlConnection() {
|
|||||||
return { url: "http://127.0.0.1:8080" };
|
return { url: "http://127.0.0.1:8080" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ConsoleSeverity = "log" | "warn" | "error";
|
||||||
|
const _oldConsoles = {
|
||||||
|
log: console.log,
|
||||||
|
warn: console.warn,
|
||||||
|
error: console.error,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function disableConsoleLog(severities: ConsoleSeverity[] = ["log", "warn"]) {
|
||||||
|
severities.forEach((severity) => {
|
||||||
|
console[severity] = () => null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function enableConsoleLog() {
|
||||||
|
Object.entries(_oldConsoles).forEach(([severity, fn]) => {
|
||||||
|
console[severity as ConsoleSeverity] = fn;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function compileQb(qb: SelectQueryBuilder<any, any, any>) {
|
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 +64,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 { auth } from "../../src/modules/middlewares";
|
import type { AuthResponse } from "../../src/auth";
|
||||||
|
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 "../../src";
|
||||||
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 () => {
|
||||||
@@ -17,8 +13,9 @@ describe("integration config", () => {
|
|||||||
|
|
||||||
// create entity
|
// create entity
|
||||||
await api.system.addConfig("data", "entities.posts", {
|
await api.system.addConfig("data", "entities.posts", {
|
||||||
|
name: "posts",
|
||||||
config: { sort_field: "id", sort_dir: "asc" },
|
config: { sort_field: "id", sort_dir: "asc" },
|
||||||
fields: { id: { type: "primary" }, asdf: { type: "text" } },
|
fields: { id: { type: "primary", name: "id" }, asdf: { type: "text" } },
|
||||||
type: "regular",
|
type: "regular",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,21 @@
|
|||||||
/// <reference types="@types/bun" />
|
/// <reference types="@types/bun" />
|
||||||
|
|
||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import { registries } from "../../src";
|
import { createApp, registries } from "../../src";
|
||||||
import { createApp } from "core/test/utils";
|
|
||||||
import { mergeObject, randomString } from "../../src/core/utils";
|
import { mergeObject, randomString } from "../../src/core/utils";
|
||||||
import type { TAppMediaConfig } from "../../src/media/media-schema";
|
import type { TAppMediaConfig } from "../../src/media/media-schema";
|
||||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||||
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,9 +39,13 @@ 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();
|
||||||
|
console.log("app", app);
|
||||||
|
|
||||||
const file = Bun.file(path);
|
const file = Bun.file(path);
|
||||||
const name = makeName("png");
|
const name = makeName("png");
|
||||||
@@ -54,6 +54,7 @@ describe("MediaController", () => {
|
|||||||
body: file,
|
body: file,
|
||||||
});
|
});
|
||||||
const result = (await res.json()) as any;
|
const result = (await res.json()) as any;
|
||||||
|
console.log(result);
|
||||||
expect(result.name).toBe(name);
|
expect(result.name).toBe(name);
|
||||||
|
|
||||||
const destFile = Bun.file(assetsTmpPath + "/" + name);
|
const destFile = Bun.file(assetsTmpPath + "/" + name);
|
||||||
@@ -94,38 +95,4 @@ describe("MediaController", () => {
|
|||||||
expect(res.status).toBe(413);
|
expect(res.status).toBe(413);
|
||||||
expect(await Bun.file(assetsTmpPath + "/" + name).exists()).toBe(false);
|
expect(await Bun.file(assetsTmpPath + "/" + name).exists()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("audio files", async () => {
|
|
||||||
const app = await makeApp();
|
|
||||||
const file = Bun.file(`${assetsPath}/test.mp3`);
|
|
||||||
const name = makeName("mp3");
|
|
||||||
const res = await app.server.request("/api/media/upload/" + name, {
|
|
||||||
method: "POST",
|
|
||||||
body: file,
|
|
||||||
});
|
|
||||||
const result = (await res.json()) as any;
|
|
||||||
expect(result.data.mime_type).toStartWith("audio/mpeg");
|
|
||||||
expect(result.name).toBe(name);
|
|
||||||
|
|
||||||
const destFile = Bun.file(assetsTmpPath + "/" + name);
|
|
||||||
expect(destFile.exists()).resolves.toBe(true);
|
|
||||||
await destFile.delete();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("text files", async () => {
|
|
||||||
const app = await makeApp();
|
|
||||||
const file = Bun.file(`${assetsPath}/test.txt`);
|
|
||||||
const name = makeName("txt");
|
|
||||||
const res = await app.server.request("/api/media/upload/" + name, {
|
|
||||||
method: "POST",
|
|
||||||
body: file,
|
|
||||||
});
|
|
||||||
const result = (await res.json()) as any;
|
|
||||||
expect(result.data.mime_type).toStartWith("text/plain");
|
|
||||||
expect(result.name).toBe(name);
|
|
||||||
|
|
||||||
const destFile = Bun.file(assetsTmpPath + "/" + name);
|
|
||||||
expect(destFile.exists()).resolves.toBe(true);
|
|
||||||
await destFile.delete();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { type FileBody, Storage } from "../../src/media/storage/Storage";
|
import { type FileBody, Storage } from "../../src/media/storage/Storage";
|
||||||
import * as StorageEvents from "../../src/media/storage/events";
|
import * as StorageEvents from "../../src/media/storage/events";
|
||||||
import { StorageAdapter } from "media/storage/StorageAdapter";
|
import { StorageAdapter } from "media";
|
||||||
|
|
||||||
class TestAdapter extends StorageAdapter {
|
class TestAdapter extends StorageAdapter {
|
||||||
files: Record<string, FileBody> = {};
|
files: Record<string, FileBody> = {};
|
||||||
|
|||||||
@@ -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");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,21 +1,13 @@
|
|||||||
import { afterAll, beforeAll, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
import { afterAll, beforeAll, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
||||||
import { createApp } from "core/test/utils";
|
import { createApp } from "../../src";
|
||||||
import { AuthController } from "../../src/auth/api/AuthController";
|
import { AuthController } from "../../src/auth/api/AuthController";
|
||||||
import { em, entity, make, text } from "data/prototype";
|
import { em, entity, make, text } from "../../src/data";
|
||||||
import { AppAuth, type ModuleBuildContext } from "modules";
|
import { AppAuth, type ModuleBuildContext } from "../../src/modules";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "../helper";
|
||||||
|
// @ts-ignore
|
||||||
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
describe("AppAuth", () => {
|
describe("AppAuth", () => {
|
||||||
test.skip("...", () => {
|
|
||||||
const auth = new AppAuth({});
|
|
||||||
console.log(auth.toJSON());
|
|
||||||
console.log(auth.config);
|
|
||||||
});
|
|
||||||
|
|
||||||
moduleTestSuite(AppAuth);
|
moduleTestSuite(AppAuth);
|
||||||
|
|
||||||
let ctx: ModuleBuildContext;
|
let ctx: ModuleBuildContext;
|
||||||
@@ -150,7 +142,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 +172,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 +196,7 @@ describe("AppAuth", () => {
|
|||||||
|
|
||||||
test("ensure user field configs is always correct", async () => {
|
test("ensure user field configs is always correct", async () => {
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
config: {
|
initialConfig: {
|
||||||
auth: {
|
auth: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { beforeEach, describe, expect, test } from "bun:test";
|
import { beforeEach, describe, expect, test } from "bun:test";
|
||||||
import { parse } from "core/utils/schema";
|
import { parse } from "../../src/core/utils";
|
||||||
import { fieldsSchema } from "../../src/data/data-schema";
|
import { fieldsSchema } from "../../src/data/data-schema";
|
||||||
import { AppData, type ModuleBuildContext } from "../../src/modules";
|
import { AppData, type ModuleBuildContext } from "../../src/modules";
|
||||||
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
||||||
|
|||||||
@@ -1,24 +1,18 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { createApp } from "core/test/utils";
|
import { createApp, registries } from "../../src";
|
||||||
import { em, entity, text } from "data/prototype";
|
import { em, entity, text } from "../../src/data";
|
||||||
import { registries } from "modules/registries";
|
|
||||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||||
import { AppMedia } from "../../src/media/AppMedia";
|
import { AppMedia } from "../../src/modules";
|
||||||
import { moduleTestSuite } from "./module-test-suite";
|
import { moduleTestSuite } from "./module-test-suite";
|
||||||
|
|
||||||
describe("AppMedia", () => {
|
describe("AppMedia", () => {
|
||||||
test.skip("...", () => {
|
|
||||||
const media = new AppMedia();
|
|
||||||
console.log(media.toJSON());
|
|
||||||
});
|
|
||||||
|
|
||||||
moduleTestSuite(AppMedia);
|
moduleTestSuite(AppMedia);
|
||||||
|
|
||||||
test("should allow additional fields", async () => {
|
test("should allow additional fields", async () => {
|
||||||
registries.media.register("local", StorageLocalAdapter);
|
registries.media.register("local", StorageLocalAdapter);
|
||||||
|
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
config: {
|
initialConfig: {
|
||||||
media: {
|
media: {
|
||||||
entity_name: "media",
|
entity_name: "media",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@@ -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,13 +1,12 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { s, stripMark } from "core/utils/schema";
|
import { stripMark } from "../../src/core/utils";
|
||||||
import { em, entity, index, text } from "data/prototype";
|
import { type TSchema, Type } from "@sinclair/typebox";
|
||||||
import { EntityManager } from "data/entities/EntityManager";
|
import { EntityManager, em, entity, index, text } from "../../src/data";
|
||||||
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
||||||
import { Module } from "../../src/modules/Module";
|
import { Module } from "../../src/modules/Module";
|
||||||
import { ModuleHelper } from "modules/ModuleHelper";
|
|
||||||
|
|
||||||
function createModule<Schema extends s.Schema>(schema: Schema) {
|
function createModule<Schema extends TSchema>(schema: Schema) {
|
||||||
return class TestModule extends Module<Schema> {
|
class TestModule extends Module<typeof schema> {
|
||||||
getSchema() {
|
getSchema() {
|
||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
@@ -17,7 +16,9 @@ function createModule<Schema extends s.Schema>(schema: Schema) {
|
|||||||
override useForceParse() {
|
override useForceParse() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
return TestModule;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Module", async () => {
|
describe("Module", async () => {
|
||||||
@@ -25,7 +26,7 @@ describe("Module", async () => {
|
|||||||
test("listener", async () => {
|
test("listener", async () => {
|
||||||
let result: any;
|
let result: any;
|
||||||
|
|
||||||
const module = createModule(s.object({ a: s.string() }));
|
const module = createModule(Type.Object({ a: Type.String() }));
|
||||||
const m = new module({ a: "test" });
|
const m = new module({ a: "test" });
|
||||||
|
|
||||||
await m.schema().set({ a: "test2" });
|
await m.schema().set({ a: "test2" });
|
||||||
@@ -41,13 +42,13 @@ describe("Module", async () => {
|
|||||||
describe("db schema", () => {
|
describe("db schema", () => {
|
||||||
class M extends Module {
|
class M extends Module {
|
||||||
override getSchema() {
|
override getSchema() {
|
||||||
return s.object({});
|
return Type.Object({});
|
||||||
}
|
}
|
||||||
|
|
||||||
prt = {
|
prt = {
|
||||||
ensureEntity: this.ctx.helper.ensureEntity.bind(this.ctx.helper),
|
ensureEntity: this.ensureEntity.bind(this),
|
||||||
ensureIndex: this.ctx.helper.ensureIndex.bind(this.ctx.helper),
|
ensureIndex: this.ensureIndex.bind(this),
|
||||||
ensureSchema: this.ctx.helper.ensureSchema.bind(this.ctx.helper),
|
ensureSchema: this.ensureSchema.bind(this),
|
||||||
};
|
};
|
||||||
|
|
||||||
get em() {
|
get em() {
|
||||||
@@ -62,11 +63,7 @@ describe("Module", async () => {
|
|||||||
_em.relations,
|
_em.relations,
|
||||||
_em.indices,
|
_em.indices,
|
||||||
);
|
);
|
||||||
const ctx = {
|
return new M({} as any, { em, flags: Module.ctx_flags } as any);
|
||||||
em,
|
|
||||||
flags: Module.ctx_flags,
|
|
||||||
};
|
|
||||||
return new M({} as any, { ...ctx, helper: new ModuleHelper(ctx as any) } as any);
|
|
||||||
}
|
}
|
||||||
function flat(_em: EntityManager) {
|
function flat(_em: EntityManager) {
|
||||||
return {
|
return {
|
||||||
@@ -146,9 +143,14 @@ describe("Module", async () => {
|
|||||||
|
|
||||||
// this should only add the field "important"
|
// this should only add the field "important"
|
||||||
m.prt.ensureEntity(
|
m.prt.ensureEntity(
|
||||||
entity("u", {
|
entity(
|
||||||
important: text(),
|
"u",
|
||||||
}),
|
{
|
||||||
|
important: text(),
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
"system",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(m.ctx.flags.sync_required).toBe(true);
|
expect(m.ctx.flags.sync_required).toBe(true);
|
||||||
@@ -157,7 +159,8 @@ describe("Module", async () => {
|
|||||||
{
|
{
|
||||||
name: "u",
|
name: "u",
|
||||||
fields: ["id", "name", "important"],
|
fields: ["id", "name", "important"],
|
||||||
type: "regular",
|
// ensured type must be present
|
||||||
|
type: "system",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "p",
|
name: "p",
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
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, stripMark } from "core/utils";
|
||||||
|
import { Type } from "@sinclair/typebox";
|
||||||
|
import { Connection, entity, text } from "data";
|
||||||
import { Module } from "modules/Module";
|
import { Module } from "modules/Module";
|
||||||
import { 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 { diff } from "core/object/diff";
|
||||||
import { Connection } from "data/connection/Connection";
|
import type { Static } from "@sinclair/typebox";
|
||||||
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 () => {
|
||||||
@@ -97,11 +92,7 @@ describe("ModuleManager", async () => {
|
|||||||
|
|
||||||
await mm2.build();
|
await mm2.build();
|
||||||
|
|
||||||
/* console.log({
|
expect(stripMark(json)).toEqual(stripMark(mm2.configs()));
|
||||||
json,
|
|
||||||
configs: mm2.configs(),
|
|
||||||
}); */
|
|
||||||
//expect(stripMark(json)).toEqual(stripMark(mm2.configs()));
|
|
||||||
expect(mm2.configs().data.entities?.test).toBeDefined();
|
expect(mm2.configs().data.entities?.test).toBeDefined();
|
||||||
expect(mm2.configs().data.entities?.test?.fields?.content).toBeDefined();
|
expect(mm2.configs().data.entities?.test?.fields?.content).toBeDefined();
|
||||||
expect(mm2.get("data").toJSON().entities?.test?.fields?.content).toBeDefined();
|
expect(mm2.get("data").toJSON().entities?.test?.fields?.content).toBeDefined();
|
||||||
@@ -138,7 +129,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
|
||||||
@@ -266,10 +257,10 @@ describe("ModuleManager", async () => {
|
|||||||
// @todo: add tests for migrations (check "backup" and new version)
|
// @todo: add tests for migrations (check "backup" and new version)
|
||||||
|
|
||||||
describe("revert", async () => {
|
describe("revert", async () => {
|
||||||
const failingModuleSchema = s.partialObject({
|
const failingModuleSchema = Type.Object({
|
||||||
value: s.number(),
|
value: Type.Optional(Type.Number()),
|
||||||
});
|
});
|
||||||
class FailingModule extends Module<s.Static<typeof failingModuleSchema>> {
|
class FailingModule extends Module<typeof failingModuleSchema> {
|
||||||
getSchema() {
|
getSchema() {
|
||||||
return failingModuleSchema;
|
return failingModuleSchema;
|
||||||
}
|
}
|
||||||
@@ -440,11 +431,11 @@ describe("ModuleManager", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("validate & revert", () => {
|
describe("validate & revert", () => {
|
||||||
const schema = s.object({
|
const schema = Type.Object({
|
||||||
value: s.array(s.number()),
|
value: Type.Array(Type.Number(), { default: [] }),
|
||||||
});
|
});
|
||||||
type SampleSchema = s.Static<typeof schema>;
|
type SampleSchema = Static<typeof schema>;
|
||||||
class Sample extends Module<SampleSchema> {
|
class Sample extends Module<typeof schema> {
|
||||||
getSchema() {
|
getSchema() {
|
||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": {} }
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
import { beforeEach, describe, expect, it } from "bun:test";
|
import { beforeEach, describe, expect, it } from "bun:test";
|
||||||
|
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { Guard } from "auth/authorize/Guard";
|
import { Guard } from "../../src/auth";
|
||||||
import { EventManager } from "core/events";
|
import { DebugLogger } from "../../src/core";
|
||||||
import { EntityManager } from "data/entities/EntityManager";
|
import { EventManager } from "../../src/core/events";
|
||||||
import { Module, type ModuleBuildContext } from "modules/Module";
|
import { Default, stripMark } from "../../src/core/utils";
|
||||||
|
import { EntityManager } from "../../src/data";
|
||||||
|
import { Module, type ModuleBuildContext } from "../../src/modules/Module";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
import { ModuleHelper } from "modules/ModuleHelper";
|
|
||||||
import { DebugLogger, McpServer } from "bknd/utils";
|
|
||||||
|
|
||||||
export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildContext {
|
export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildContext {
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
const ctx = {
|
return {
|
||||||
connection: dummyConnection,
|
connection: dummyConnection,
|
||||||
server: new Hono(),
|
server: new Hono(),
|
||||||
em: new EntityManager([], dummyConnection),
|
em: new EntityManager([], dummyConnection),
|
||||||
@@ -19,13 +19,8 @@ export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildCon
|
|||||||
guard: new Guard(),
|
guard: new Guard(),
|
||||||
flags: Module.ctx_flags,
|
flags: Module.ctx_flags,
|
||||||
logger: new DebugLogger(false),
|
logger: new DebugLogger(false),
|
||||||
mcp: new McpServer(),
|
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
return {
|
|
||||||
...ctx,
|
|
||||||
helper: new ModuleHelper(ctx as any),
|
|
||||||
} as any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function moduleTestSuite(module: { new (): Module }) {
|
export function moduleTestSuite(module: { new (): Module }) {
|
||||||
@@ -45,8 +40,7 @@ export function moduleTestSuite(module: { new (): Module }) {
|
|||||||
it("uses the default config", async () => {
|
it("uses the default config", async () => {
|
||||||
const m = new module();
|
const m = new module();
|
||||||
await m.setContext(ctx).build();
|
await m.setContext(ctx).build();
|
||||||
expect(m.toJSON()).toEqual(m.getSchema().template({}, { withOptional: true }));
|
expect(stripMark(m.toJSON())).toEqual(Default(m.getSchema(), {}));
|
||||||
//expect(stripMark(m.toJSON())).toEqual(Default(m.getSchema(), {}));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
+54
-107
@@ -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,25 @@ 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 = ["bun:test", "node:test", "node:assert/strict", "@libsql/client"] as const;
|
||||||
...dependencies,
|
|
||||||
"bun:test",
|
|
||||||
"node:test",
|
|
||||||
"node:assert/strict",
|
|
||||||
"@libsql/client",
|
|
||||||
"bknd",
|
|
||||||
/^bknd\/.*/,
|
|
||||||
"jsonv-ts",
|
|
||||||
/^jsonv-ts\/.*/,
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Building backend and general API
|
* Building backend and general API
|
||||||
*/
|
*/
|
||||||
async function buildApi() {
|
async function buildApi() {
|
||||||
|
banner("Building API");
|
||||||
await tsup.build({
|
await tsup.build({
|
||||||
minify,
|
minify,
|
||||||
sourcemap,
|
sourcemap,
|
||||||
@@ -87,23 +74,23 @@ async function buildApi() {
|
|||||||
define,
|
define,
|
||||||
entry: [
|
entry: [
|
||||||
"src/index.ts",
|
"src/index.ts",
|
||||||
|
"src/core/index.ts",
|
||||||
"src/core/utils/index.ts",
|
"src/core/utils/index.ts",
|
||||||
"src/plugins/index.ts",
|
"src/data/index.ts",
|
||||||
"src/modes/index.ts",
|
"src/media/index.ts",
|
||||||
],
|
],
|
||||||
outDir: "dist",
|
outDir: "dist",
|
||||||
external: [...external],
|
external: [...external],
|
||||||
metafile: true,
|
metafile: true,
|
||||||
target: "esnext",
|
|
||||||
platform: "browser",
|
platform: "browser",
|
||||||
format: ["esm"],
|
format: ["esm"],
|
||||||
splitting: false,
|
splitting: false,
|
||||||
|
treeshake: true,
|
||||||
loader: {
|
loader: {
|
||||||
".svg": "dataurl",
|
".svg": "dataurl",
|
||||||
},
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
delayTypes();
|
delayTypes();
|
||||||
oldConsole.log(c.cyan("[API]"), c.green("built"));
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -147,6 +134,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"],
|
||||||
@@ -154,10 +142,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"],
|
||||||
@@ -165,7 +153,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"));
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -176,6 +163,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,
|
||||||
@@ -209,7 +197,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"));
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -230,7 +217,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: {
|
||||||
@@ -239,90 +225,51 @@ function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsu
|
|||||||
},
|
},
|
||||||
external: [
|
external: [
|
||||||
/^cloudflare*/,
|
/^cloudflare*/,
|
||||||
/^@?hono.*?/,
|
/^@?(hono|libsql).*?/,
|
||||||
/^(bknd|react|next|node).*?/,
|
/^(bknd|react|next|node).*?/,
|
||||||
/.*\.(html)$/,
|
/.*\.(html)$/,
|
||||||
...external,
|
|
||||||
...(Array.isArray(overrides.external) ? overrides.external : []),
|
...(Array.isArray(overrides.external) ? overrides.external : []),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildAdapters() {
|
async function buildAdapters() {
|
||||||
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", {
|
await tsup.build(baseConfig("astro"));
|
||||||
external: [/^bun\:.*/],
|
await tsup.build(baseConfig("aws"));
|
||||||
}),
|
await tsup.build(
|
||||||
),
|
baseConfig("cloudflare", {
|
||||||
tsup.build(baseConfig("astro")),
|
external: [/^kysely/],
|
||||||
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({
|
await tsup.build({
|
||||||
...baseConfig("nextjs"),
|
...baseConfig("vite"),
|
||||||
platform: "node",
|
platform: "node",
|
||||||
}),
|
});
|
||||||
|
|
||||||
tsup.build({
|
await tsup.build({
|
||||||
...baseConfig("node"),
|
...baseConfig("nextjs"),
|
||||||
platform: "node",
|
platform: "node",
|
||||||
}),
|
});
|
||||||
|
|
||||||
tsup.build({
|
await tsup.build({
|
||||||
...baseConfig("sqlite/edge"),
|
...baseConfig("node"),
|
||||||
entry: ["src/adapter/sqlite/edge.ts"],
|
platform: "node",
|
||||||
outDir: "dist/adapter/sqlite",
|
});
|
||||||
metafile: false,
|
|
||||||
}),
|
|
||||||
|
|
||||||
tsup.build({
|
|
||||||
...baseConfig("sqlite/node"),
|
|
||||||
entry: ["src/adapter/sqlite/node.ts"],
|
|
||||||
outDir: "dist/adapter/sqlite",
|
|
||||||
platform: "node",
|
|
||||||
metafile: false,
|
|
||||||
}),
|
|
||||||
|
|
||||||
tsup.build({
|
|
||||||
...baseConfig("sqlite/bun"),
|
|
||||||
entry: ["src/adapter/sqlite/bun.ts"],
|
|
||||||
outDir: "dist/adapter/sqlite",
|
|
||||||
metafile: false,
|
|
||||||
external: [/^bun\:.*/],
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all([buildApi(), buildUi(), buildUiElements(), buildAdapters()]);
|
await buildApi();
|
||||||
|
await buildUi();
|
||||||
|
await buildUiElements();
|
||||||
|
await buildAdapters();
|
||||||
|
|||||||
+1
-2
@@ -2,5 +2,4 @@
|
|||||||
#registry = "http://localhost:4873"
|
#registry = "http://localhost:4873"
|
||||||
|
|
||||||
[test]
|
[test]
|
||||||
coverageSkipTestFiles = true
|
coverageSkipTestFiles = true
|
||||||
console.depth = 10
|
|
||||||
+7
-11
@@ -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,
|
||||||
@@ -142,7 +142,6 @@ const adapters = {
|
|||||||
},
|
},
|
||||||
nextjs: {
|
nextjs: {
|
||||||
dir: path.join(basePath, "examples/nextjs"),
|
dir: path.join(basePath, "examples/nextjs"),
|
||||||
env: "TEST_TIMEOUT=20000",
|
|
||||||
clean: async function () {
|
clean: async function () {
|
||||||
const cwd = path.relative(process.cwd(), this.dir);
|
const cwd = path.relative(process.cwd(), this.dir);
|
||||||
await $`cd ${cwd} && rm -rf .nextjs data.db`;
|
await $`cd ${cwd} && rm -rf .nextjs data.db`;
|
||||||
@@ -189,26 +188,23 @@ 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 : "";
|
await $`TEST_URL=${data} TEST_ADAPTER=${name} bun run test:e2e`;
|
||||||
await $`TEST_URL=${data} TEST_ADAPTER=${name} ${add_env} bun run test:e2e`;
|
console.log("DONE!");
|
||||||
console.info("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...");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// run with: TEST_ADAPTER=astro bun run e2e/adapters.ts
|
|
||||||
// (modify `test:e2e` to `test:e2e:ui` to see the UI)
|
|
||||||
if (process.env.TEST_ADAPTER) {
|
if (process.env.TEST_ADAPTER) {
|
||||||
await testAdapter(process.env.TEST_ADAPTER as any);
|
await testAdapter(process.env.TEST_ADAPTER as any);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import { createApp } from "bknd/adapter/bun";
|
|
||||||
|
|
||||||
async function generate() {
|
|
||||||
console.info("Generating MCP documentation...");
|
|
||||||
const app = await createApp({
|
|
||||||
connection: {
|
|
||||||
url: ":memory:",
|
|
||||||
},
|
|
||||||
config: {
|
|
||||||
server: {
|
|
||||||
mcp: {
|
|
||||||
enabled: true,
|
|
||||||
path: "/mcp2",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
auth: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
media: {
|
|
||||||
enabled: true,
|
|
||||||
adapter: {
|
|
||||||
type: "local",
|
|
||||||
config: {
|
|
||||||
path: "./",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await app.build();
|
|
||||||
await app.getMcpClient().ping();
|
|
||||||
|
|
||||||
const { tools, resources } = app.mcp!.toJSON();
|
|
||||||
await Bun.write("../docs/mcp.json", JSON.stringify({ tools, resources }, null, 2));
|
|
||||||
|
|
||||||
console.info("MCP documentation generated.");
|
|
||||||
}
|
|
||||||
|
|
||||||
void generate();
|
|
||||||
+37
-74
@@ -3,7 +3,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"bin": "./dist/cli/index.js",
|
"bin": "./dist/cli/index.js",
|
||||||
"version": "0.19.0",
|
"version": "0.14.0-rc.2",
|
||||||
"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,10 +13,6 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/bknd-io/bknd/issues"
|
"url": "https://github.com/bknd-io/bknd/issues"
|
||||||
},
|
},
|
||||||
"packageManager": "bun@1.2.22",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=22.13"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "BKND_CLI_LOG_LEVEL=debug vite",
|
"dev": "BKND_CLI_LOG_LEVEL=debug vite",
|
||||||
"build": "NODE_ENV=production bun run build.ts --minify --types",
|
"build": "NODE_ENV=production bun run build.ts --minify --types",
|
||||||
@@ -30,21 +26,22 @@
|
|||||||
"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",
|
||||||
"test:bun": "ALL_TESTS=1 bun test --bail",
|
"test:bun": "ALL_TESTS=1 bun test --bail",
|
||||||
"test:node": "vitest run",
|
"test:node": "tsx --test $(find . -type f -name '*.native-spec.ts')",
|
||||||
"test:adapters": "bun test src/adapter/**/*.adapter.spec.ts --bail",
|
"test:adapters": "bun test src/adapter/**/*.adapter.spec.ts --bail",
|
||||||
"test:coverage": "ALL_TESTS=1 bun test --bail --coverage",
|
"test:coverage": "ALL_TESTS=1 bun test --bail --coverage",
|
||||||
|
"test:vitest": "vitest run",
|
||||||
|
"test:vitest:watch": "vitest",
|
||||||
"test:vitest:coverage": "vitest run --coverage",
|
"test:vitest:coverage": "vitest run --coverage",
|
||||||
"test:e2e": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
"test:e2e:adapters": "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": {
|
||||||
@@ -53,8 +50,10 @@
|
|||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@hello-pangea/dnd": "^18.0.1",
|
"@hello-pangea/dnd": "^18.0.1",
|
||||||
"@hono/swagger-ui": "^0.5.1",
|
"@hono/swagger-ui": "^0.5.1",
|
||||||
|
"@libsql/client": "^0.15.2",
|
||||||
"@mantine/core": "^7.17.1",
|
"@mantine/core": "^7.17.1",
|
||||||
"@mantine/hooks": "^7.17.1",
|
"@mantine/hooks": "^7.17.1",
|
||||||
|
"@sinclair/typebox": "0.34.30",
|
||||||
"@tanstack/react-form": "^1.0.5",
|
"@tanstack/react-form": "^1.0.5",
|
||||||
"@uiw/react-codemirror": "^4.23.10",
|
"@uiw/react-codemirror": "^4.23.10",
|
||||||
"@xyflow/react": "^12.4.4",
|
"@xyflow/react": "^12.4.4",
|
||||||
@@ -62,33 +61,30 @@
|
|||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"fast-xml-parser": "^5.0.8",
|
"fast-xml-parser": "^5.0.8",
|
||||||
"hono": "4.8.3",
|
"json-schema-form-react": "^0.0.2",
|
||||||
"json-schema-library": "10.0.0-rc7",
|
"json-schema-library": "10.0.0-rc7",
|
||||||
"json-schema-to-ts": "^3.1.1",
|
"json-schema-to-ts": "^3.1.1",
|
||||||
"jsonv-ts": "0.9.1",
|
"kysely": "^0.27.6",
|
||||||
"kysely": "0.27.6",
|
"hono": "^4.7.11",
|
||||||
"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"
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"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.9.3",
|
|
||||||
"@cloudflare/workers-types": "^4.20250606.0",
|
|
||||||
"@dagrejs/dagre": "^1.1.4",
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
"@hono/vite-dev-server": "^0.21.0",
|
"@hono/typebox-validator": "^0.3.3",
|
||||||
|
"@hono/vite-dev-server": "^0.19.1",
|
||||||
"@hookform/resolvers": "^4.1.3",
|
"@hookform/resolvers": "^4.1.3",
|
||||||
"@libsql/client": "^0.15.9",
|
"@libsql/kysely-libsql": "^0.4.1",
|
||||||
"@mantine/modals": "^7.17.1",
|
"@mantine/modals": "^7.17.1",
|
||||||
"@mantine/notifications": "^7.17.1",
|
"@mantine/notifications": "^7.17.1",
|
||||||
"@playwright/test": "^1.51.1",
|
"@playwright/test": "^1.51.1",
|
||||||
"@rjsf/core": "5.22.2",
|
"@rjsf/core": "5.22.2",
|
||||||
"@standard-schema/spec": "^1.0.0",
|
|
||||||
"@tabler/icons-react": "3.18.0",
|
"@tabler/icons-react": "3.18.0",
|
||||||
"@tailwindcss/postcss": "^4.0.12",
|
"@tailwindcss/postcss": "^4.0.12",
|
||||||
"@tailwindcss/vite": "^4.0.12",
|
"@tailwindcss/vite": "^4.0.12",
|
||||||
@@ -104,11 +100,11 @@
|
|||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"jotai": "^2.12.2",
|
"jotai": "^2.12.2",
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.0.0",
|
||||||
|
"jsonv-ts": "^0.1.0",
|
||||||
"kysely-d1": "^0.3.0",
|
"kysely-d1": "^0.3.0",
|
||||||
"kysely-generic-sqlite": "^1.2.1",
|
|
||||||
"libsql-stateless-easy": "^1.8.0",
|
|
||||||
"open": "^10.1.0",
|
"open": "^10.1.0",
|
||||||
"openapi-types": "^12.1.3",
|
"openapi-types": "^12.1.3",
|
||||||
|
"picocolors": "^1.1.1",
|
||||||
"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",
|
||||||
@@ -125,14 +121,11 @@
|
|||||||
"tsc-alias": "^1.8.11",
|
"tsc-alias": "^1.8.11",
|
||||||
"tsup": "^8.4.0",
|
"tsup": "^8.4.0",
|
||||||
"tsx": "^4.19.3",
|
"tsx": "^4.19.3",
|
||||||
"uuid": "^11.1.0",
|
|
||||||
"vite": "^6.3.5",
|
"vite": "^6.3.5",
|
||||||
"vite-plugin-circular-dependency": "^0.5.0",
|
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
"vitest": "^3.0.9",
|
"vitest": "^3.0.9",
|
||||||
"wouter": "^3.6.0",
|
"wouter": "^3.6.0",
|
||||||
"wrangler": "^4.37.1",
|
"@cloudflare/workers-types": "^4.20250606.0"
|
||||||
"miniflare": "^4.20250913.0"
|
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@hono/node-server": "^1.14.3"
|
"@hono/node-server": "^1.14.3"
|
||||||
@@ -165,6 +158,16 @@
|
|||||||
"import": "./dist/ui/client/index.js",
|
"import": "./dist/ui/client/index.js",
|
||||||
"require": "./dist/ui/client/index.js"
|
"require": "./dist/ui/client/index.js"
|
||||||
},
|
},
|
||||||
|
"./data": {
|
||||||
|
"types": "./dist/types/data/index.d.ts",
|
||||||
|
"import": "./dist/data/index.js",
|
||||||
|
"require": "./dist/data/index.js"
|
||||||
|
},
|
||||||
|
"./core": {
|
||||||
|
"types": "./dist/types/core/index.d.ts",
|
||||||
|
"import": "./dist/core/index.js",
|
||||||
|
"require": "./dist/core/index.js"
|
||||||
|
},
|
||||||
"./utils": {
|
"./utils": {
|
||||||
"types": "./dist/types/core/utils/index.d.ts",
|
"types": "./dist/types/core/utils/index.d.ts",
|
||||||
"import": "./dist/core/utils/index.js",
|
"import": "./dist/core/utils/index.js",
|
||||||
@@ -175,40 +178,16 @@
|
|||||||
"import": "./dist/cli/index.js",
|
"import": "./dist/cli/index.js",
|
||||||
"require": "./dist/cli/index.js"
|
"require": "./dist/cli/index.js"
|
||||||
},
|
},
|
||||||
"./plugins": {
|
"./media": {
|
||||||
"types": "./dist/types/plugins/index.d.ts",
|
"types": "./dist/types/media/index.d.ts",
|
||||||
"import": "./dist/plugins/index.js",
|
"import": "./dist/media/index.js",
|
||||||
"require": "./dist/plugins/index.js"
|
"require": "./dist/media/index.js"
|
||||||
},
|
|
||||||
"./modes": {
|
|
||||||
"types": "./dist/types/modes/index.d.ts",
|
|
||||||
"import": "./dist/modes/index.js",
|
|
||||||
"require": "./dist/modes/index.js"
|
|
||||||
},
|
|
||||||
"./adapter/sqlite": {
|
|
||||||
"types": "./dist/types/adapter/sqlite/edge.d.ts",
|
|
||||||
"import": {
|
|
||||||
"workerd": "./dist/adapter/sqlite/edge.js",
|
|
||||||
"edge-light": "./dist/adapter/sqlite/edge.js",
|
|
||||||
"netlify": "./dist/adapter/sqlite/edge.js",
|
|
||||||
"vercel": "./dist/adapter/sqlite/edge.js",
|
|
||||||
"browser": "./dist/adapter/sqlite/edge.js",
|
|
||||||
"bun": "./dist/adapter/sqlite/bun.js",
|
|
||||||
"node": "./dist/adapter/sqlite/node.js",
|
|
||||||
"default": "./dist/adapter/sqlite/node.js"
|
|
||||||
},
|
|
||||||
"require": "./dist/adapter/sqlite/node.js"
|
|
||||||
},
|
},
|
||||||
"./adapter/cloudflare": {
|
"./adapter/cloudflare": {
|
||||||
"types": "./dist/types/adapter/cloudflare/index.d.ts",
|
"types": "./dist/types/adapter/cloudflare/index.d.ts",
|
||||||
"import": "./dist/adapter/cloudflare/index.js",
|
"import": "./dist/adapter/cloudflare/index.js",
|
||||||
"require": "./dist/adapter/cloudflare/index.js"
|
"require": "./dist/adapter/cloudflare/index.js"
|
||||||
},
|
},
|
||||||
"./adapter/cloudflare/proxy": {
|
|
||||||
"types": "./dist/types/adapter/cloudflare/proxy.d.ts",
|
|
||||||
"import": "./dist/adapter/cloudflare/proxy.js",
|
|
||||||
"require": "./dist/adapter/cloudflare/proxy.js"
|
|
||||||
},
|
|
||||||
"./adapter": {
|
"./adapter": {
|
||||||
"types": "./dist/types/adapter/index.d.ts",
|
"types": "./dist/types/adapter/index.d.ts",
|
||||||
"import": "./dist/adapter/index.js"
|
"import": "./dist/adapter/index.js"
|
||||||
@@ -250,23 +229,7 @@
|
|||||||
},
|
},
|
||||||
"./dist/main.css": "./dist/ui/main.css",
|
"./dist/main.css": "./dist/ui/main.css",
|
||||||
"./dist/styles.css": "./dist/ui/styles.css",
|
"./dist/styles.css": "./dist/ui/styles.css",
|
||||||
"./dist/manifest.json": "./dist/static/.vite/manifest.json",
|
"./dist/manifest.json": "./dist/static/.vite/manifest.json"
|
||||||
"./static/*": "./dist/static/*"
|
|
||||||
},
|
|
||||||
"typesVersions": {
|
|
||||||
"*": {
|
|
||||||
"utils": ["./dist/types/core/utils/index.d.ts"],
|
|
||||||
"cli": ["./dist/types/cli/index.d.ts"],
|
|
||||||
"plugins": ["./dist/types/plugins/index.d.ts"],
|
|
||||||
"adapter": ["./dist/types/adapter/index.d.ts"],
|
|
||||||
"adapter/cloudflare": ["./dist/types/adapter/cloudflare/index.d.ts"],
|
|
||||||
"adapter/vite": ["./dist/types/adapter/vite/index.d.ts"],
|
|
||||||
"adapter/nextjs": ["./dist/types/adapter/nextjs/index.d.ts"],
|
|
||||||
"adapter/react-router": ["./dist/types/adapter/react-router/index.d.ts"],
|
|
||||||
"adapter/bun": ["./dist/types/adapter/bun/index.d.ts"],
|
|
||||||
"adapter/node": ["./dist/types/adapter/node/index.d.ts"],
|
|
||||||
"adapter/sqlite": ["./dist/types/adapter/sqlite/edge.d.ts"]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { defineConfig, devices } from "@playwright/test";
|
|||||||
const baseUrl = process.env.TEST_URL || "http://localhost:28623";
|
const baseUrl = process.env.TEST_URL || "http://localhost:28623";
|
||||||
const startCommand = process.env.TEST_START_COMMAND || "bun run dev";
|
const startCommand = process.env.TEST_START_COMMAND || "bun run dev";
|
||||||
const autoStart = ["1", "true", undefined].includes(process.env.TEST_AUTO_START);
|
const autoStart = ["1", "true", undefined].includes(process.env.TEST_AUTO_START);
|
||||||
const timeout = process.env.TEST_TIMEOUT ? Number.parseInt(process.env.TEST_TIMEOUT) : 5000;
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testMatch: "**/*.e2e-spec.ts",
|
testMatch: "**/*.e2e-spec.ts",
|
||||||
@@ -13,7 +12,7 @@ export default defineConfig({
|
|||||||
retries: process.env.CI ? 2 : 0,
|
retries: process.env.CI ? 2 : 0,
|
||||||
workers: process.env.CI ? 1 : undefined,
|
workers: process.env.CI ? 1 : undefined,
|
||||||
reporter: "html",
|
reporter: "html",
|
||||||
timeout,
|
timeout: 20000,
|
||||||
use: {
|
use: {
|
||||||
baseURL: baseUrl,
|
baseURL: baseUrl,
|
||||||
trace: "on-first-retry",
|
trace: "on-first-retry",
|
||||||
|
|||||||
+36
-50
@@ -1,10 +1,10 @@
|
|||||||
import type { SafeUser } from "bknd";
|
import type { SafeUser } from "auth";
|
||||||
import { AuthApi, type AuthApiOptions } from "auth/api/AuthApi";
|
import { AuthApi, type AuthApiOptions } from "auth/api/AuthApi";
|
||||||
import { DataApi, type DataApiOptions } from "data/api/DataApi";
|
import { DataApi, type DataApiOptions } from "data/api/DataApi";
|
||||||
import { decode } from "hono/jwt";
|
import { decode } from "hono/jwt";
|
||||||
import { MediaApi, type MediaApiOptions } from "media/api/MediaApi";
|
import { MediaApi, type MediaApiOptions } from "media/api/MediaApi";
|
||||||
import { SystemApi } from "modules/SystemApi";
|
import { SystemApi } from "modules/SystemApi";
|
||||||
import { omitKeys } from "bknd/utils";
|
import { omitKeys } from "core/utils";
|
||||||
import type { BaseModuleApiOptions } from "modules";
|
import type { BaseModuleApiOptions } from "modules";
|
||||||
|
|
||||||
export type TApiUser = SafeUser;
|
export type TApiUser = SafeUser;
|
||||||
@@ -40,11 +40,10 @@ export type ApiOptions = {
|
|||||||
data?: SubApiOptions<DataApiOptions>;
|
data?: SubApiOptions<DataApiOptions>;
|
||||||
auth?: SubApiOptions<AuthApiOptions>;
|
auth?: SubApiOptions<AuthApiOptions>;
|
||||||
media?: SubApiOptions<MediaApiOptions>;
|
media?: SubApiOptions<MediaApiOptions>;
|
||||||
credentials?: RequestCredentials;
|
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
token?: string;
|
token?: string;
|
||||||
user?: TApiUser | null;
|
user?: TApiUser;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
request: Request;
|
request: Request;
|
||||||
@@ -68,7 +67,7 @@ export class Api {
|
|||||||
public auth!: AuthApi;
|
public auth!: AuthApi;
|
||||||
public media!: MediaApi;
|
public media!: MediaApi;
|
||||||
|
|
||||||
constructor(public options: ApiOptions = {}) {
|
constructor(private options: ApiOptions = {}) {
|
||||||
// only mark verified if forced
|
// only mark verified if forced
|
||||||
this.verified = options.verified === true;
|
this.verified = options.verified === true;
|
||||||
|
|
||||||
@@ -130,45 +129,29 @@ export class Api {
|
|||||||
} else if (this.storage) {
|
} else if (this.storage) {
|
||||||
this.storage.getItem(this.tokenKey).then((token) => {
|
this.storage.getItem(this.tokenKey).then((token) => {
|
||||||
this.token_transport = "header";
|
this.token_transport = "header";
|
||||||
this.updateToken(token ? String(token) : undefined, {
|
this.updateToken(token ? String(token) : undefined);
|
||||||
verified: true,
|
|
||||||
trigger: false,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Make storage async to allow async storages even if sync given
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private get storage() {
|
private get storage() {
|
||||||
const storage = this.options.storage;
|
if (!this.options.storage) return null;
|
||||||
return new Proxy(
|
return {
|
||||||
{},
|
getItem: async (key: string) => {
|
||||||
{
|
return await this.options.storage!.getItem(key);
|
||||||
get(_, prop) {
|
|
||||||
return (...args: any[]) => {
|
|
||||||
const response = storage ? storage[prop](...args) : undefined;
|
|
||||||
if (response instanceof Promise) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
// biome-ignore lint/suspicious/noThenProperty: it's a promise :)
|
|
||||||
then: (fn) => fn(response),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
) as any;
|
setItem: async (key: string, value: string) => {
|
||||||
|
return await this.options.storage!.setItem(key, value);
|
||||||
|
},
|
||||||
|
removeItem: async (key: string) => {
|
||||||
|
return await this.options.storage!.removeItem(key);
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
updateToken(
|
updateToken(token?: string, opts?: { rebuild?: boolean; trigger?: boolean }) {
|
||||||
token?: string,
|
|
||||||
opts?: { rebuild?: boolean; verified?: boolean; trigger?: boolean },
|
|
||||||
) {
|
|
||||||
this.token = token;
|
this.token = token;
|
||||||
this.verified = opts?.verified === true;
|
this.verified = false;
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
this.user = omitKeys(decode(token).payload as any, ["iat", "iss", "exp"]) as any;
|
this.user = omitKeys(decode(token).payload as any, ["iat", "iss", "exp"]) as any;
|
||||||
@@ -176,22 +159,21 @@ export class Api {
|
|||||||
this.user = undefined;
|
this.user = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const emit = () => {
|
|
||||||
if (opts?.trigger !== false) {
|
|
||||||
this.options.onAuthStateChange?.(this.getAuthState());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (this.storage) {
|
if (this.storage) {
|
||||||
const key = this.tokenKey;
|
const key = this.tokenKey;
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
this.storage.setItem(key, token).then(emit);
|
this.storage.setItem(key, token).then(() => {
|
||||||
|
this.options.onAuthStateChange?.(this.getAuthState());
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.storage.removeItem(key).then(emit);
|
this.storage.removeItem(key).then(() => {
|
||||||
|
this.options.onAuthStateChange?.(this.getAuthState());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (opts?.trigger !== false) {
|
if (opts?.trigger !== false) {
|
||||||
emit();
|
this.options.onAuthStateChange?.(this.getAuthState());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +182,6 @@ export class Api {
|
|||||||
|
|
||||||
private markAuthVerified(verfied: boolean) {
|
private markAuthVerified(verfied: boolean) {
|
||||||
this.verified = verfied;
|
this.verified = verfied;
|
||||||
this.options.onAuthStateChange?.(this.getAuthState());
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,6 +208,11 @@ export class Api {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async verifyAuth() {
|
async verifyAuth() {
|
||||||
|
if (!this.token) {
|
||||||
|
this.markAuthVerified(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { ok, data } = await this.auth.me();
|
const { ok, data } = await this.auth.me();
|
||||||
const user = data?.user;
|
const user = data?.user;
|
||||||
@@ -235,10 +221,10 @@ export class Api {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.user = user;
|
this.user = user;
|
||||||
} catch (e) {
|
|
||||||
this.updateToken(undefined);
|
|
||||||
} finally {
|
|
||||||
this.markAuthVerified(true);
|
this.markAuthVerified(true);
|
||||||
|
} catch (e) {
|
||||||
|
this.markAuthVerified(false);
|
||||||
|
this.updateToken(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,7 +239,6 @@ export class Api {
|
|||||||
headers: this.options.headers,
|
headers: this.options.headers,
|
||||||
token_transport: this.token_transport,
|
token_transport: this.token_transport,
|
||||||
verbose: this.options.verbose,
|
verbose: this.options.verbose,
|
||||||
credentials: this.options.credentials,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,9 +257,10 @@ export class Api {
|
|||||||
this.auth = new AuthApi(
|
this.auth = new AuthApi(
|
||||||
{
|
{
|
||||||
...baseParams,
|
...baseParams,
|
||||||
|
credentials: this.options.storage ? "omit" : "include",
|
||||||
...this.options.auth,
|
...this.options.auth,
|
||||||
onTokenUpdate: (token, verified) => {
|
onTokenUpdate: (token) => {
|
||||||
this.updateToken(token, { rebuild: true, verified, trigger: true });
|
this.updateToken(token, { rebuild: true });
|
||||||
this.options.auth?.onTokenUpdate?.(token);
|
this.options.auth?.onTokenUpdate?.(token);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+66
-173
@@ -1,70 +1,30 @@
|
|||||||
import type { CreateUserPayload } from "auth/AppAuth";
|
import type { CreateUserPayload } from "auth/AppAuth";
|
||||||
import { $console, McpClient } from "bknd/utils";
|
import { $console } from "core";
|
||||||
import { Event } from "core/events";
|
import { Event } from "core/events";
|
||||||
import type { em as prototypeEm } from "data/prototype";
|
import { Connection, type LibSqlCredentials, LibsqlConnection } from "data";
|
||||||
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 { ServerEnv } from "modules/Controller";
|
|
||||||
import type { IEmailDriver, ICacheDriver } from "core/drivers";
|
|
||||||
|
|
||||||
// biome-ignore format: must be here
|
// biome-ignore format: must be here
|
||||||
import { Api, type ApiOptions } from "Api";
|
import { Api, type ApiOptions } from "Api";
|
||||||
|
import type { ServerEnv } from "modules/Controller";
|
||||||
|
|
||||||
export type AppPluginConfig = {
|
export type AppPlugin = (app: App) => Promise<void> | void;
|
||||||
/**
|
|
||||||
* The name of the plugin.
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
/**
|
|
||||||
* The schema of the plugin.
|
|
||||||
*/
|
|
||||||
schema?: () => MaybePromise<ReturnType<typeof prototypeEm> | void>;
|
|
||||||
/**
|
|
||||||
* Called before the app is built.
|
|
||||||
*/
|
|
||||||
beforeBuild?: () => MaybePromise<void>;
|
|
||||||
/**
|
|
||||||
* Called after the app is built.
|
|
||||||
*/
|
|
||||||
onBuilt?: () => MaybePromise<void>;
|
|
||||||
/**
|
|
||||||
* Called when the server is initialized.
|
|
||||||
*/
|
|
||||||
onServerInit?: (server: Hono<ServerEnv>) => MaybePromise<void>;
|
|
||||||
/**
|
|
||||||
* Called when the app is booted.
|
|
||||||
*/
|
|
||||||
onBoot?: () => MaybePromise<void>;
|
|
||||||
/**
|
|
||||||
* Called when the app is first booted.
|
|
||||||
*/
|
|
||||||
onFirstBoot?: () => MaybePromise<void>;
|
|
||||||
};
|
|
||||||
export type AppPlugin = (app: App) => AppPluginConfig;
|
|
||||||
|
|
||||||
abstract class AppEvent<A = {}> extends Event<{ app: App } & A> {}
|
abstract class AppEvent<A = {}> extends Event<{ app: App } & A> {}
|
||||||
export class AppConfigUpdatedEvent extends AppEvent<{
|
export class AppConfigUpdatedEvent extends AppEvent {
|
||||||
module: string;
|
|
||||||
config: ModuleConfigs[keyof ModuleConfigs];
|
|
||||||
}> {
|
|
||||||
static override slug = "app-config-updated";
|
static override slug = "app-config-updated";
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* @type {Event<{ app: App }>}
|
|
||||||
*/
|
|
||||||
export class AppBuiltEvent extends AppEvent {
|
export class AppBuiltEvent extends AppEvent {
|
||||||
static override slug = "app-built";
|
static override slug = "app-built";
|
||||||
}
|
}
|
||||||
@@ -90,108 +50,55 @@ export type AppOptions = {
|
|||||||
seed?: (ctx: ModuleBuildContext & { app: App }) => Promise<void>;
|
seed?: (ctx: ModuleBuildContext & { app: App }) => Promise<void>;
|
||||||
manager?: Omit<ModuleManagerOptions, "initial" | "onUpdated" | "seed">;
|
manager?: Omit<ModuleManagerOptions, "initial" | "onUpdated" | "seed">;
|
||||||
asyncEventsMode?: "sync" | "async" | "none";
|
asyncEventsMode?: "sync" | "async" | "none";
|
||||||
drivers?: {
|
|
||||||
email?: IEmailDriver;
|
|
||||||
cache?: ICacheDriver;
|
|
||||||
};
|
|
||||||
mode?: "db" | "code";
|
|
||||||
readonly?: boolean;
|
|
||||||
};
|
};
|
||||||
export type CreateAppConfig = {
|
export type CreateAppConfig = {
|
||||||
connection?: Connection | { url: string };
|
connection?:
|
||||||
config?: PartialRec<ModuleConfigs>;
|
| Connection
|
||||||
|
| {
|
||||||
|
// @deprecated
|
||||||
|
type: "libsql";
|
||||||
|
config: LibSqlCredentials;
|
||||||
|
}
|
||||||
|
| LibSqlCredentials;
|
||||||
|
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,
|
|
||||||
Config extends PartialRec<ModuleConfigs> = PartialRec<ModuleConfigs>,
|
|
||||||
Options extends AppOptions = AppOptions,
|
|
||||||
> {
|
|
||||||
static readonly Events = AppEvents;
|
static readonly Events = AppEvents;
|
||||||
|
|
||||||
modules: ModuleManager;
|
modules: ModuleManager;
|
||||||
adminController?: AdminController;
|
adminController?: AdminController;
|
||||||
_id: string = crypto.randomUUID();
|
_id: string = crypto.randomUUID();
|
||||||
plugins: Map<string, AppPluginConfig> = new Map();
|
|
||||||
drivers: Options["drivers"] = {};
|
|
||||||
|
|
||||||
private trigger_first_boot = false;
|
private trigger_first_boot = false;
|
||||||
|
private plugins: AppPlugin[];
|
||||||
private _building: boolean = false;
|
private _building: boolean = false;
|
||||||
private _systemController: SystemController | null = null;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public connection: C,
|
private connection: Connection,
|
||||||
_config?: Config,
|
_initialConfig?: InitialModuleConfigs,
|
||||||
public options?: Options,
|
private options?: AppOptions,
|
||||||
) {
|
) {
|
||||||
this.drivers = options?.drivers ?? {};
|
this.plugins = options?.plugins ?? [];
|
||||||
|
this.modules = new ModuleManager(connection, {
|
||||||
for (const plugin of options?.plugins ?? []) {
|
|
||||||
const config = plugin(this);
|
|
||||||
if (this.plugins.has(config.name)) {
|
|
||||||
throw new Error(`Plugin ${config.name} already registered`);
|
|
||||||
}
|
|
||||||
this.plugins.set(config.name, config);
|
|
||||||
}
|
|
||||||
this.runPlugins("onBoot");
|
|
||||||
|
|
||||||
// use db manager by default
|
|
||||||
const Manager = this.mode === "db" ? DbModuleManager : ModuleManager;
|
|
||||||
|
|
||||||
this.modules = new Manager(connection, {
|
|
||||||
...(options?.manager ?? {}),
|
...(options?.manager ?? {}),
|
||||||
initial: _config,
|
initial: _initialConfig,
|
||||||
onUpdated: this.onUpdated.bind(this),
|
onUpdated: this.onUpdated.bind(this),
|
||||||
onFirstBoot: this.onFirstBoot.bind(this),
|
onFirstBoot: this.onFirstBoot.bind(this),
|
||||||
onServerInit: this.onServerInit.bind(this),
|
onServerInit: this.onServerInit.bind(this),
|
||||||
onModulesBuilt: this.onModulesBuilt.bind(this),
|
|
||||||
});
|
});
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async runPlugins<Key extends keyof AppPluginConfig>(
|
async build(options?: { sync?: boolean; fetch?: boolean; forceBuild?: boolean }) {
|
||||||
key: Key,
|
|
||||||
...args: any[]
|
|
||||||
): Promise<{ name: string; result: any }[]> {
|
|
||||||
const results: { name: string; result: any }[] = [];
|
|
||||||
for (const [name, config] of this.plugins) {
|
|
||||||
try {
|
|
||||||
if (key in config && config[key]) {
|
|
||||||
const fn = config[key];
|
|
||||||
if (fn && typeof fn === "function") {
|
|
||||||
$console.debug(`[Plugin:${name}] ${key}`);
|
|
||||||
// @ts-expect-error
|
|
||||||
const result = await fn(...args);
|
|
||||||
results.push({
|
|
||||||
name,
|
|
||||||
result,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
$console.warn(`[Plugin:${name}] error running "${key}"`, String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
async build(options?: { sync?: boolean; forceBuild?: boolean; [key: string]: any }) {
|
|
||||||
// prevent multiple concurrent builds
|
// prevent multiple concurrent builds
|
||||||
if (this._building) {
|
if (this._building) {
|
||||||
while (this._building) {
|
while (this._building) {
|
||||||
@@ -199,24 +106,24 @@ export class App<
|
|||||||
}
|
}
|
||||||
if (!options?.forceBuild) return;
|
if (!options?.forceBuild) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.runPlugins("beforeBuild");
|
|
||||||
this._building = true;
|
this._building = true;
|
||||||
|
|
||||||
if (options?.sync) this.modules.ctx().flags.sync_required = true;
|
if (options?.sync) this.modules.ctx().flags.sync_required = true;
|
||||||
await this.modules.build();
|
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);
|
|
||||||
|
// load plugins
|
||||||
|
if (this.plugins.length > 0) {
|
||||||
|
await Promise.all(this.plugins.map((plugin) => plugin(this)));
|
||||||
|
}
|
||||||
|
|
||||||
// emit built event
|
|
||||||
$console.log("App built");
|
$console.log("App built");
|
||||||
await this.emgr.emit(new AppBuiltEvent({ app: this }));
|
await this.emgr.emit(new AppBuiltEvent({ app: this }));
|
||||||
await this.runPlugins("onBuilt");
|
|
||||||
|
|
||||||
// first boot is set from ModuleManager when there wasn't a config table
|
// first boot is set from ModuleManager when there wasn't a config table
|
||||||
if (this.trigger_first_boot) {
|
if (this.trigger_first_boot) {
|
||||||
@@ -231,6 +138,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,14 +150,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()) {
|
|
||||||
console.error("App is not built yet, run build() first");
|
|
||||||
}
|
|
||||||
return this.server.fetch as any;
|
return this.server.fetch as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,7 +199,6 @@ export class App<
|
|||||||
return this.module.auth.createUser(p);
|
return this.module.auth.createUser(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
// @todo: potentially add option to clone the app, so that when used in listeners, it won't trigger listeners
|
|
||||||
getApi(options?: LocalApiOptions) {
|
getApi(options?: LocalApiOptions) {
|
||||||
const fetcher = this.server.request as typeof fetch;
|
const fetcher = this.server.request as typeof fetch;
|
||||||
if (options && options instanceof Request) {
|
if (options && options instanceof Request) {
|
||||||
@@ -305,19 +208,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");
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = new URL(config.path, "http://localhost").toString();
|
|
||||||
return new McpClient({
|
|
||||||
url,
|
|
||||||
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".
|
||||||
@@ -330,16 +220,15 @@ export class App<
|
|||||||
$console.log("App config updated", module);
|
$console.log("App config updated", module);
|
||||||
// @todo: potentially double syncing
|
// @todo: potentially double syncing
|
||||||
await this.build({ sync: true });
|
await this.build({ sync: true });
|
||||||
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this, module, config }));
|
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this }));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async onFirstBoot() {
|
async onFirstBoot() {
|
||||||
$console.log("App first boot");
|
$console.log("App first boot");
|
||||||
this.trigger_first_boot = true;
|
this.trigger_first_boot = true;
|
||||||
await this.runPlugins("onFirstBoot");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async onServerInit(server: Hono<ServerEnv>) {
|
async onServerInit(server: Hono<ServerEnv>) {
|
||||||
server.use(async (c, next) => {
|
server.use(async (c, next) => {
|
||||||
c.set("app", this);
|
c.set("app", this);
|
||||||
await this.emgr.emit(new AppRequest({ app: this, request: c.req.raw }));
|
await this.emgr.emit(new AppRequest({ app: this, request: c.req.raw }));
|
||||||
@@ -369,31 +258,35 @@ export class App<
|
|||||||
if (this.options?.manager?.onServerInit) {
|
if (this.options?.manager?.onServerInit) {
|
||||||
this.options.manager.onServerInit(server);
|
this.options.manager.onServerInit(server);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.runPlugins("onServerInit", server);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async onModulesBuilt(ctx: ModuleBuildContext) {
|
|
||||||
const results = (await this.runPlugins("schema")) as {
|
|
||||||
name: string;
|
|
||||||
result: ReturnType<typeof prototypeEm>;
|
|
||||||
}[];
|
|
||||||
if (results.length > 0) {
|
|
||||||
for (const { name, result } of results) {
|
|
||||||
if (result) {
|
|
||||||
$console.log(`[Plugin:${name}] schema`);
|
|
||||||
ctx.helper.ensureSchema(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await this.options?.manager?.onModulesBuilt?.(ctx);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createApp(config: CreateAppConfig = {}) {
|
export function createApp(config: CreateAppConfig = {}) {
|
||||||
if (!config.connection || !Connection.isConnection(config.connection)) {
|
let connection: Connection | undefined = undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (Connection.isConnection(config.connection)) {
|
||||||
|
connection = config.connection;
|
||||||
|
} else if (typeof config.connection === "object") {
|
||||||
|
if ("type" in config.connection) {
|
||||||
|
$console.warn(
|
||||||
|
"Using deprecated connection type 'libsql', use the 'config' object directly.",
|
||||||
|
);
|
||||||
|
connection = new LibsqlConnection(config.connection.config);
|
||||||
|
} else {
|
||||||
|
connection = new LibsqlConnection(config.connection);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
connection = new LibsqlConnection({ url: ":memory:" });
|
||||||
|
$console.warn("No connection provided, using in-memory database");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$console.error("Could not create connection", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!connection) {
|
||||||
throw new Error("Invalid connection");
|
throw new Error("Invalid connection");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new App(config.connection, config.config, config.options);
|
return new App(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 = {
|
||||||
@@ -8,16 +8,18 @@ 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 = import.meta.env as Env,
|
args: Env = {} as Env,
|
||||||
|
opts: FrameworkOptions = {},
|
||||||
) {
|
) {
|
||||||
return await createFrameworkApp(config, args);
|
return await createFrameworkApp(config, args ?? import.meta.env, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serve<Env = AstroEnv>(
|
export function serve<Env = AstroEnv>(
|
||||||
config: AstroBkndConfig<Env> = {},
|
config: AstroBkndConfig<Env> = {},
|
||||||
args: Env = import.meta.env as 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,9 +1,9 @@
|
|||||||
/// <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 "bknd/adapter/node";
|
||||||
import { config, type App } from "bknd";
|
import { config } from "bknd/core";
|
||||||
import type { ServeOptions } from "bun";
|
import type { ServeOptions } from "bun";
|
||||||
import { serveStatic } from "hono/bun";
|
import { serveStatic } from "hono/bun";
|
||||||
|
|
||||||
@@ -11,34 +11,30 @@ 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 = Bun.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,
|
|
||||||
}),
|
|
||||||
...config,
|
...config,
|
||||||
|
serveStatic: serveStatic({ root }),
|
||||||
},
|
},
|
||||||
args,
|
args ?? (process.env as Env),
|
||||||
|
opts,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createHandler<Env = BunEnv>(
|
export function createHandler<Env = BunEnv>(
|
||||||
config: BunBkndConfig<Env> = {},
|
config: BunBkndConfig<Env> = {},
|
||||||
args: Env = Bun.env as Env,
|
args: Env = {} as Env,
|
||||||
|
opts?: RuntimeOptions,
|
||||||
) {
|
) {
|
||||||
let app: App | undefined;
|
|
||||||
return async (req: Request) => {
|
return async (req: Request) => {
|
||||||
if (!app) {
|
const app = await createApp(config, args ?? (process.env as Env), opts);
|
||||||
app = await createApp(config, args);
|
|
||||||
}
|
|
||||||
return app.fetch(req);
|
return app.fetch(req);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -47,17 +43,16 @@ export function serve<Env = BunEnv>(
|
|||||||
{
|
{
|
||||||
distPath,
|
distPath,
|
||||||
connection,
|
connection,
|
||||||
config: _config,
|
initialConfig,
|
||||||
options,
|
options,
|
||||||
port = config.server.default_port,
|
port = config.server.default_port,
|
||||||
onBuilt,
|
onBuilt,
|
||||||
buildConfig,
|
buildConfig,
|
||||||
adminOptions,
|
adminOptions,
|
||||||
serveStatic,
|
|
||||||
beforeBuild,
|
|
||||||
...serveOptions
|
...serveOptions
|
||||||
}: BunBkndConfig<Env> = {},
|
}: BunBkndConfig<Env> = {},
|
||||||
args: Env = Bun.env as Env,
|
args: Env = {} as Env,
|
||||||
|
opts?: RuntimeOptions,
|
||||||
) {
|
) {
|
||||||
Bun.serve({
|
Bun.serve({
|
||||||
...serveOptions,
|
...serveOptions,
|
||||||
@@ -65,18 +60,17 @@ export function serve<Env = BunEnv>(
|
|||||||
fetch: createHandler(
|
fetch: createHandler(
|
||||||
{
|
{
|
||||||
connection,
|
connection,
|
||||||
config: _config,
|
initialConfig,
|
||||||
options,
|
options,
|
||||||
onBuilt,
|
onBuilt,
|
||||||
buildConfig,
|
buildConfig,
|
||||||
adminOptions,
|
adminOptions,
|
||||||
distPath,
|
distPath,
|
||||||
serveStatic,
|
|
||||||
beforeBuild,
|
|
||||||
},
|
},
|
||||||
args,
|
args,
|
||||||
|
opts,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
console.info(`Server is running on http://localhost:${port}`);
|
console.log(`Server is running on http://localhost:${port}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { connectionTestSuite } from "data/connection/connection-test-suite";
|
|
||||||
import { bunSqlite } from "./BunSqliteConnection";
|
|
||||||
import { bunTestRunner } from "adapter/bun/test";
|
|
||||||
import { describe, test, mock, expect } from "bun:test";
|
|
||||||
import { Database } from "bun:sqlite";
|
|
||||||
import { GenericSqliteConnection } from "data/connection/sqlite/GenericSqliteConnection";
|
|
||||||
|
|
||||||
describe("BunSqliteConnection", () => {
|
|
||||||
connectionTestSuite(bunTestRunner, {
|
|
||||||
makeConnection: () => ({
|
|
||||||
connection: bunSqlite({ database: new Database(":memory:") }),
|
|
||||||
dispose: async () => {},
|
|
||||||
}),
|
|
||||||
rawDialectDetails: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
test("onCreateConnection", async () => {
|
|
||||||
const called = mock(() => null);
|
|
||||||
|
|
||||||
const conn = bunSqlite({
|
|
||||||
onCreateConnection: (db) => {
|
|
||||||
expect(db).toBeInstanceOf(Database);
|
|
||||||
called();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await conn.ping();
|
|
||||||
|
|
||||||
expect(conn).toBeInstanceOf(GenericSqliteConnection);
|
|
||||||
expect(conn.db).toBeInstanceOf(Database);
|
|
||||||
expect(called).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user