mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 08:06:00 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3180037b67 |
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"bknd": {
|
|
||||||
"url": "http://localhost:28623/api/system/mcp"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,28 +12,15 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "22.x"
|
|
||||||
|
|
||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: oven-sh/setup-bun@v1
|
uses: oven-sh/setup-bun@v1
|
||||||
with:
|
with:
|
||||||
bun-version: "1.3.3"
|
bun-version: latest
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
working-directory: ./app
|
working-directory: ./app
|
||||||
run: bun install
|
run: bun install
|
||||||
|
|
||||||
- name: Build
|
- name: Run tests
|
||||||
working-directory: ./app
|
working-directory: ./app
|
||||||
run: bun run build:ci
|
run: bun run test
|
||||||
|
|
||||||
- name: Run Bun tests
|
|
||||||
working-directory: ./app
|
|
||||||
run: bun run test:bun
|
|
||||||
|
|
||||||
- name: Run Node tests
|
|
||||||
working-directory: ./app
|
|
||||||
run: npm run test:node
|
|
||||||
+2
-7
@@ -27,11 +27,6 @@ packages/media/.env
|
|||||||
.npmrc
|
.npmrc
|
||||||
/.verdaccio
|
/.verdaccio
|
||||||
.idea
|
.idea
|
||||||
.vscode/*
|
.vscode
|
||||||
!.vscode/settings.json
|
|
||||||
.git_old
|
.git_old
|
||||||
docker/tmp
|
docker/tmp
|
||||||
.debug
|
|
||||||
.history
|
|
||||||
.aider*
|
|
||||||
.vercel
|
|
||||||
Vendored
-19
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
|
||||||
"biome.enabled": true,
|
|
||||||
"editor.defaultFormatter": "biomejs.biome",
|
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
//"source.organizeImports.biome": "explicit",
|
|
||||||
"source.fixAll.biome": "explicit"
|
|
||||||
},
|
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
|
||||||
"typescript.preferences.autoImportFileExcludePatterns": [
|
|
||||||
"**/dist/**",
|
|
||||||
"**/node_modules/**/dist/**",
|
|
||||||
"**/node_modules/**/!(src|lib|esm)/**" // optional, stricter
|
|
||||||
],
|
|
||||||
"typescript.preferences.includePackageJsonAutoImports": "on",
|
|
||||||
"typescript.tsserver.watchOptions": {
|
|
||||||
"excludeDirectories": ["**/dist", "**/node_modules/**/dist"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +1,31 @@
|
|||||||
[](https://npmjs.org/package/bknd)
|
[](https://npmjs.org/package/bknd)
|
||||||
|
[](https://www.npmjs.com/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.
|
||||||
|
|
||||||
Depending on what you use, the size can be higher as additional dependencies are getting pulled in. The minimal size of a full `bknd` app as an API is around 300 kB gzipped (e.g. deployed as Cloudflare Worker).
|
Depending on what you use, the size can be higher as additional dependencies are getting pulled in. The minimal size of a full `bknd` app as an API is around 212 kB gzipped (e.g. deployed as Cloudflare Worker).
|
||||||
|
|
||||||
## Motivation
|
## Motivation
|
||||||
Creating digital products always requires developing both the backend (the logic) and the frontend (the appearance). Building a backend from scratch demands deep knowledge in areas such as authentication and database management. Using a backend framework can speed up initial development, but it still requires ongoing effort to work within its constraints (e.g., *"how to do X with Y?"*), which can quickly slow you down. Choosing a backend system is a tough decision, as you might not be aware of its limitations until you encounter them.
|
Creating digital products always requires developing both the backend (the logic) and the frontend (the appearance). Building a backend from scratch demands deep knowledge in areas such as authentication and database management. Using a backend framework can speed up initial development, but it still requires ongoing effort to work within its constraints (e.g., *"how to do X with Y?"*), which can quickly slow you down. Choosing a backend system is a tough decision, as you might not be aware of its limitations until you encounter them.
|
||||||
@@ -62,7 +39,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)
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
# ===== DB Settings =====
|
|
||||||
VITE_DB_URL=:memory:
|
|
||||||
# you can set a location for a database here, it'll overwrite the previous setting
|
|
||||||
# ideally use the ".db" folder (create it first), it's git ignored
|
|
||||||
VITE_DB_URL=file:.db/dev.db
|
|
||||||
|
|
||||||
# alternatively, you can use url/token combination
|
|
||||||
#VITE_DB_URL=
|
|
||||||
#VITE_DB_TOKEN=
|
|
||||||
|
|
||||||
|
|
||||||
# ===== DEV Server =====
|
|
||||||
# restart the dev server on every change (enable with "1")
|
|
||||||
VITE_APP_FRESH=
|
|
||||||
# displays react-scan widget (enable with "1")
|
|
||||||
VITE_DEBUG_RERENDERS=
|
|
||||||
# console logs registered routes on start (enable with "1")
|
|
||||||
VITE_SHOW_ROUTES=
|
|
||||||
|
|
||||||
|
|
||||||
# ===== Test Credentials =====
|
|
||||||
RESEND_API_KEY=
|
|
||||||
PLUNK_API_KEY=
|
|
||||||
R2_TOKEN=
|
|
||||||
|
|
||||||
R2_ACCESS_KEY=
|
|
||||||
R2_SECRET_ACCESS_KEY=
|
|
||||||
R2_URL=
|
|
||||||
|
|
||||||
AWS_ACCESS_KEY=
|
|
||||||
AWS_SECRET_KEY=
|
|
||||||
AWS_S3_URL=
|
|
||||||
|
|
||||||
OAUTH_CLIENT_ID=
|
|
||||||
OAUTH_CLIENT_SECRET=
|
|
||||||
|
|
||||||
PUBLIC_POSTHOG_KEY=
|
|
||||||
PUBLIC_POSTHOG_HOST=
|
|
||||||
|
|
||||||
# ===== Internals =====
|
|
||||||
BKND_CLI_CREATE_REF=main
|
|
||||||
BKND_CLI_LOG_LEVEL=debug
|
|
||||||
BKND_MODULES_DEBUG=1
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
playwright-report
|
|
||||||
test-results
|
|
||||||
bknd.config.*
|
|
||||||
__test__/helper.d.ts
|
|
||||||
Vendored
-12
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"typescript.preferences.includePackageJsonAutoImports": "off",
|
|
||||||
"typescript.suggest.autoImports": true,
|
|
||||||
"typescript.preferences.importModuleSpecifier": "relative",
|
|
||||||
"search.exclude": {
|
|
||||||
"**/dist/**": true,
|
|
||||||
"**/node_modules/**": true
|
|
||||||
},
|
|
||||||
"files.exclude": {
|
|
||||||
"**/dist/**": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+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("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.
|
Before Width: | Height: | Size: 16 KiB |
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
hello
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import { expect, describe, it, beforeAll, afterAll, mock } from "bun:test";
|
|
||||||
import * as adapter from "adapter";
|
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
|
||||||
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
|
||||||
import { bunTestRunner } from "adapter/bun/test";
|
|
||||||
import { omitKeys } from "core/utils";
|
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
describe("adapter", () => {
|
|
||||||
it("makes config", async () => {
|
|
||||||
expect(omitKeys(await adapter.makeConfig({}), ["connection"])).toEqual({});
|
|
||||||
expect(
|
|
||||||
omitKeys(await adapter.makeConfig({}, { env: { TEST: "test" } }), ["connection"]),
|
|
||||||
).toEqual({});
|
|
||||||
|
|
||||||
// merges everything returned from `app` with the config
|
|
||||||
expect(
|
|
||||||
omitKeys(
|
|
||||||
await adapter.makeConfig(
|
|
||||||
{ 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 () => {
|
|
||||||
const called = mock(() => null);
|
|
||||||
const config = await adapter.makeConfig(
|
|
||||||
{
|
|
||||||
app: (env) => ({
|
|
||||||
connection: { url: "test" },
|
|
||||||
config: { server: { cors: { origin: "test" } } },
|
|
||||||
options: {
|
|
||||||
mode: "db",
|
|
||||||
},
|
|
||||||
onBuilt: () => {
|
|
||||||
called();
|
|
||||||
expect(env).toEqual({ foo: "bar" });
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{ foo: "bar" },
|
|
||||||
);
|
|
||||||
expect(config.connection).toEqual({ url: "test" });
|
|
||||||
expect(config.config).toEqual({ server: { cors: { origin: "test" } } });
|
|
||||||
expect(config.options).toEqual({ mode: "db" });
|
|
||||||
await config.onBuilt?.(null as any);
|
|
||||||
expect(called).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
adapterTestSuite(bunTestRunner, {
|
|
||||||
makeApp: adapter.createFrameworkApp,
|
|
||||||
label: "framework app",
|
|
||||||
});
|
|
||||||
|
|
||||||
adapterTestSuite(bunTestRunner, {
|
|
||||||
makeApp: adapter.createRuntimeApp,
|
|
||||||
label: "runtime app",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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,11 @@
|
|||||||
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 { disableConsoleLog, enableConsoleLog, schemaToEm } from "../helper";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
import { parse } from "core/utils/schema";
|
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
beforeAll(disableConsoleLog);
|
||||||
afterAll(enableConsoleLog);
|
afterAll(enableConsoleLog);
|
||||||
@@ -65,15 +64,6 @@ describe("DataApi", () => {
|
|||||||
const res = await req;
|
const res = await req;
|
||||||
expect(res.data).toEqual(payload as any);
|
expect(res.data).toEqual(payload as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
// make sure sort is working
|
|
||||||
const req = await api.readMany("posts", {
|
|
||||||
select: ["title"],
|
|
||||||
sort: "-id",
|
|
||||||
});
|
|
||||||
expect(req.data).toEqual(payload.reverse() as any);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("updates many", async () => {
|
it("updates many", async () => {
|
||||||
@@ -153,7 +143,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 +192,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,8 +1,8 @@
|
|||||||
/// <reference types="@types/bun" />
|
/// <reference types="@types/bun" />
|
||||||
import { describe, expect, it } from "bun:test";
|
import { describe, expect, it } from "bun:test";
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { getFileFromContext, isFile, isReadableStream } from "core/utils";
|
import { getFileFromContext, isFile, isReadableStream } from "../../src/core/utils";
|
||||||
import { MediaApi } from "media/api/MediaApi";
|
import { MediaApi } from "../../src/media/api/MediaApi";
|
||||||
import { assetsPath, assetsTmpPath } from "../helper";
|
import { assetsPath, assetsTmpPath } from "../helper";
|
||||||
|
|
||||||
const mockedBackend = new Hono()
|
const mockedBackend = new Hono()
|
||||||
@@ -15,7 +15,7 @@ const mockedBackend = new Hono()
|
|||||||
.get("/file/:name", async (c) => {
|
.get("/file/:name", async (c) => {
|
||||||
const { name } = c.req.param();
|
const { name } = c.req.param();
|
||||||
const file = Bun.file(`${assetsPath}/${name}`);
|
const file = Bun.file(`${assetsPath}/${name}`);
|
||||||
return new Response(new File([await file.bytes()], name, { type: file.type }), {
|
return new Response(file, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": file.type,
|
"Content-Type": file.type,
|
||||||
"Content-Length": file.size.toString(),
|
"Content-Length": file.size.toString(),
|
||||||
@@ -39,28 +39,10 @@ describe("MediaApi", () => {
|
|||||||
// @ts-ignore tests
|
// @ts-ignore tests
|
||||||
const api = new MediaApi({
|
const api = new MediaApi({
|
||||||
token: "token",
|
token: "token",
|
||||||
token_transport: "header",
|
|
||||||
});
|
});
|
||||||
expect(api.getUploadHeaders().get("Authorization")).toBe("Bearer token");
|
expect(api.getUploadHeaders().get("Authorization")).toBe("Bearer token");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return empty headers if not using `header` transport", () => {
|
|
||||||
expect(
|
|
||||||
new MediaApi({
|
|
||||||
token_transport: "cookie",
|
|
||||||
})
|
|
||||||
.getUploadHeaders()
|
|
||||||
.has("Authorization"),
|
|
||||||
).toBe(false);
|
|
||||||
expect(
|
|
||||||
new MediaApi({
|
|
||||||
token_transport: "none",
|
|
||||||
})
|
|
||||||
.getUploadHeaders()
|
|
||||||
.has("Authorization"),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should get file: native", async () => {
|
it("should get file: native", async () => {
|
||||||
const name = "image.png";
|
const name = "image.png";
|
||||||
const path = `${assetsTmpPath}/${name}`;
|
const path = `${assetsTmpPath}/${name}`;
|
||||||
@@ -121,12 +103,8 @@ describe("MediaApi", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should upload file in various ways", async () => {
|
it("should upload file in various ways", async () => {
|
||||||
const api = new MediaApi(
|
// @ts-ignore tests
|
||||||
{
|
const api = new MediaApi({}, mockedBackend.request);
|
||||||
upload_fetcher: mockedBackend.request,
|
|
||||||
},
|
|
||||||
mockedBackend.request,
|
|
||||||
);
|
|
||||||
const file = Bun.file(`${assetsPath}/image.png`);
|
const file = Bun.file(`${assetsPath}/image.png`);
|
||||||
|
|
||||||
async function matches(req: Promise<any>, filename: string) {
|
async function matches(req: Promise<any>, filename: string) {
|
||||||
|
|||||||
@@ -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 { type 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", {
|
||||||
@@ -67,104 +51,4 @@ describe("App", () => {
|
|||||||
expect(todos[0]?.title).toBe("ctx");
|
expect(todos[0]?.title).toBe("ctx");
|
||||||
expect(todos[1]?.title).toBe("api");
|
expect(todos[1]?.title).toBe("api");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("lifecycle events are triggered", async () => {
|
|
||||||
const firstBoot = mock(() => null);
|
|
||||||
const configUpdate = mock(() => null);
|
|
||||||
const appBuilt = mock(() => null);
|
|
||||||
const appRequest = mock(() => null);
|
|
||||||
const beforeResponse = mock(() => null);
|
|
||||||
|
|
||||||
const app = createApp();
|
|
||||||
|
|
||||||
app.emgr.onEvent(
|
|
||||||
App.Events.AppFirstBoot,
|
|
||||||
(event) => {
|
|
||||||
expect(event).toBeInstanceOf(App.Events.AppFirstBoot);
|
|
||||||
expect(event.params.app.version()).toBe(app.version());
|
|
||||||
firstBoot();
|
|
||||||
},
|
|
||||||
"sync",
|
|
||||||
);
|
|
||||||
app.emgr.onEvent(
|
|
||||||
App.Events.AppBuiltEvent,
|
|
||||||
(event) => {
|
|
||||||
expect(event).toBeInstanceOf(App.Events.AppBuiltEvent);
|
|
||||||
expect(event.params.app.version()).toBe(app.version());
|
|
||||||
appBuilt();
|
|
||||||
},
|
|
||||||
"sync",
|
|
||||||
);
|
|
||||||
app.emgr.onEvent(
|
|
||||||
App.Events.AppConfigUpdatedEvent,
|
|
||||||
() => {
|
|
||||||
configUpdate();
|
|
||||||
},
|
|
||||||
"sync",
|
|
||||||
);
|
|
||||||
app.emgr.onEvent(
|
|
||||||
App.Events.AppRequest,
|
|
||||||
(event) => {
|
|
||||||
expect(event).toBeInstanceOf(App.Events.AppRequest);
|
|
||||||
expect(event.params.app.version()).toBe(app.version());
|
|
||||||
expect(event.params.request).toBeInstanceOf(Request);
|
|
||||||
appRequest();
|
|
||||||
},
|
|
||||||
"sync",
|
|
||||||
);
|
|
||||||
app.emgr.onEvent(
|
|
||||||
App.Events.AppBeforeResponse,
|
|
||||||
(event) => {
|
|
||||||
expect(event).toBeInstanceOf(App.Events.AppBeforeResponse);
|
|
||||||
expect(event.params.app.version()).toBe(app.version());
|
|
||||||
expect(event.params.response).toBeInstanceOf(Response);
|
|
||||||
beforeResponse();
|
|
||||||
},
|
|
||||||
"sync",
|
|
||||||
);
|
|
||||||
|
|
||||||
await app.build();
|
|
||||||
expect(firstBoot).toHaveBeenCalled();
|
|
||||||
expect(appBuilt).toHaveBeenCalled();
|
|
||||||
//expect(configUpdate).toHaveBeenCalled();
|
|
||||||
expect(appRequest).not.toHaveBeenCalled();
|
|
||||||
expect(beforeResponse).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("emgr exec modes", async () => {
|
|
||||||
const called = mock(() => null);
|
|
||||||
const app = createApp({
|
|
||||||
options: {
|
|
||||||
asyncEventsMode: "sync",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// register async listener
|
|
||||||
app.emgr.onEvent(App.Events.AppFirstBoot, async () => {
|
|
||||||
called();
|
|
||||||
});
|
|
||||||
|
|
||||||
await app.build();
|
|
||||||
await app.server.request(new Request("http://localhost"));
|
|
||||||
|
|
||||||
// expect async listeners to be executed sync after request
|
|
||||||
expect(called).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("getMcpClient", async () => {
|
|
||||||
const app = createApp({
|
|
||||||
config: {
|
|
||||||
server: {
|
|
||||||
mcp: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await app.build();
|
|
||||||
const client = app.getMcpClient();
|
|
||||||
const res = await client.listTools();
|
|
||||||
expect(res).toBeDefined();
|
|
||||||
expect(res?.tools.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,42 +0,0 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
|
||||||
import { code, hybrid } from "modes";
|
|
||||||
|
|
||||||
describe("modes", () => {
|
|
||||||
describe("code", () => {
|
|
||||||
test("verify base configuration", async () => {
|
|
||||||
const c = code({}) as any;
|
|
||||||
const config = await c.app?.({} as any);
|
|
||||||
expect(Object.keys(config)).toEqual(["options"]);
|
|
||||||
expect(config.options.mode).toEqual("code");
|
|
||||||
expect(config.options.plugins).toEqual([]);
|
|
||||||
expect(config.options.manager.skipValidation).toEqual(false);
|
|
||||||
expect(config.options.manager.onModulesBuilt).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("keeps overrides", async () => {
|
|
||||||
const c = code({
|
|
||||||
connection: {
|
|
||||||
url: ":memory:",
|
|
||||||
},
|
|
||||||
}) as any;
|
|
||||||
const config = await c.app?.({} as any);
|
|
||||||
expect(config.connection.url).toEqual(":memory:");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("hybrid", () => {
|
|
||||||
test("fails if no reader is provided", () => {
|
|
||||||
// @ts-ignore
|
|
||||||
expect(hybrid({} as any).app?.({} as any)).rejects.toThrow(/reader/);
|
|
||||||
});
|
|
||||||
test("verify base configuration", async () => {
|
|
||||||
const c = hybrid({ reader: async () => ({}) }) as any;
|
|
||||||
const config = await c.app?.({} as any);
|
|
||||||
expect(Object.keys(config)).toEqual(["reader", "beforeBuild", "config", "options"]);
|
|
||||||
expect(config.options.mode).toEqual("db");
|
|
||||||
expect(config.options.plugins).toEqual([]);
|
|
||||||
expect(config.options.manager.skipValidation).toEqual(false);
|
|
||||||
expect(config.options.manager.onModulesBuilt).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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 "../../src/media/storage/adapters/StorageLocalAdapter";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
describe("repros", async () => {
|
describe("repros", async () => {
|
||||||
/**
|
/**
|
||||||
@@ -76,7 +71,7 @@ describe("repros", async () => {
|
|||||||
expect(app.em.entities.map((e) => e.name)).toEqual(["media", "test"]);
|
expect(app.em.entities.map((e) => e.name)).toEqual(["media", "test"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("verify inversedBy", async () => {
|
test.only("verify inversedBy", async () => {
|
||||||
const schema = proto.em(
|
const schema = proto.em(
|
||||||
{
|
{
|
||||||
products: proto.entity("products", {
|
products: proto.entity("products", {
|
||||||
@@ -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());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -3,10 +3,8 @@ import { OAuthStrategy } from "../../../src/auth/authenticate/strategies";
|
|||||||
|
|
||||||
const ALL_TESTS = !!process.env.ALL_TESTS;
|
const ALL_TESTS = !!process.env.ALL_TESTS;
|
||||||
|
|
||||||
// @todo: add mock response
|
|
||||||
describe("OAuthStrategy", async () => {
|
describe("OAuthStrategy", async () => {
|
||||||
return;
|
const strategy = new OAuthStrategy({
|
||||||
/*const strategy = new OAuthStrategy({
|
|
||||||
type: "oidc",
|
type: "oidc",
|
||||||
client: {
|
client: {
|
||||||
client_id: process.env.OAUTH_CLIENT_ID!,
|
client_id: process.env.OAUTH_CLIENT_ID!,
|
||||||
@@ -23,7 +21,6 @@ describe("OAuthStrategy", async () => {
|
|||||||
|
|
||||||
const server = Bun.serve({
|
const server = Bun.serve({
|
||||||
fetch: async (req) => {
|
fetch: async (req) => {
|
||||||
console.log("req", req.method, req.url);
|
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
if (url.pathname === "/auth/google/callback") {
|
if (url.pathname === "/auth/google/callback") {
|
||||||
console.log("req", req);
|
console.log("req", req);
|
||||||
@@ -45,5 +42,5 @@ describe("OAuthStrategy", async () => {
|
|||||||
console.log("request", request);
|
console.log("request", request);
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100000));
|
await new Promise((resolve) => setTimeout(resolve, 100000));
|
||||||
});*/
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -70,9 +70,6 @@ describe("EventManager", async () => {
|
|||||||
new SpecialEvent({ foo: "bar" });
|
new SpecialEvent({ foo: "bar" });
|
||||||
new InformationalEvent();
|
new InformationalEvent();
|
||||||
|
|
||||||
// execute asyncs
|
|
||||||
await emgr.executeAsyncs();
|
|
||||||
|
|
||||||
expect(call).toHaveBeenCalledTimes(2);
|
expect(call).toHaveBeenCalledTimes(2);
|
||||||
expect(delayed).toHaveBeenCalled();
|
expect(delayed).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -83,11 +80,15 @@ describe("EventManager", async () => {
|
|||||||
call();
|
call();
|
||||||
return Promise.all(p);
|
return Promise.all(p);
|
||||||
};
|
};
|
||||||
const emgr = new EventManager({ InformationalEvent });
|
const emgr = new EventManager(
|
||||||
|
{ InformationalEvent },
|
||||||
|
{
|
||||||
|
asyncExecutor,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
emgr.onEvent(InformationalEvent, async () => {});
|
emgr.onEvent(InformationalEvent, async () => {});
|
||||||
await emgr.emit(new InformationalEvent());
|
await emgr.emit(new InformationalEvent());
|
||||||
await emgr.executeAsyncs(asyncExecutor);
|
|
||||||
expect(call).toHaveBeenCalled();
|
expect(call).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -124,9 +125,6 @@ describe("EventManager", async () => {
|
|||||||
const e2 = await emgr.emit(new ReturnEvent({ foo: "bar" }));
|
const e2 = await emgr.emit(new ReturnEvent({ foo: "bar" }));
|
||||||
expect(e2.returned).toBe(true);
|
expect(e2.returned).toBe(true);
|
||||||
expect(e2.params.foo).toBe("bar-1-0");
|
expect(e2.params.foo).toBe("bar-1-0");
|
||||||
|
|
||||||
await emgr.executeAsyncs();
|
|
||||||
|
|
||||||
expect(onInvalidReturn).toHaveBeenCalled();
|
expect(onInvalidReturn).toHaveBeenCalled();
|
||||||
expect(asyncEventCallback).toHaveBeenCalled();
|
expect(asyncEventCallback).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Registry } from "core/registry/Registry";
|
import type { TObject, TString } from "@sinclair/typebox";
|
||||||
import { s } from "core/utils/schema";
|
import { Registry } from "../../src/core/registry/Registry";
|
||||||
|
import { type TSchema, Type } from "../../src/core/utils";
|
||||||
|
|
||||||
type Constructor<T> = new (...args: any[]) => T;
|
type Constructor<T> = new (...args: any[]) => T;
|
||||||
|
|
||||||
@@ -11,7 +12,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 +20,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 +29,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 +38,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 +47,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 +57,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 +82,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()),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import * as assert from "node:assert/strict";
|
||||||
|
import { createWriteStream } from "node:fs";
|
||||||
|
import { after, beforeEach, describe, test } from "node:test";
|
||||||
|
import { Miniflare } from "miniflare";
|
||||||
|
import {
|
||||||
|
CloudflareKVCacheItem,
|
||||||
|
CloudflareKVCachePool,
|
||||||
|
} from "../../../src/core/cache/adapters/CloudflareKvCache";
|
||||||
|
import { runTests } from "./cache-test-suite";
|
||||||
|
|
||||||
|
// https://github.com/nodejs/node/issues/44372#issuecomment-1736530480
|
||||||
|
console.log = async (message: any) => {
|
||||||
|
const tty = createWriteStream("/dev/tty");
|
||||||
|
const msg = typeof message === "string" ? message : JSON.stringify(message, null, 2);
|
||||||
|
return tty.write(`${msg}\n`);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("CloudflareKv", async () => {
|
||||||
|
let mf: Miniflare;
|
||||||
|
runTests({
|
||||||
|
createCache: async () => {
|
||||||
|
if (mf) {
|
||||||
|
await mf.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
mf = new Miniflare({
|
||||||
|
modules: true,
|
||||||
|
script: "export default { async fetch() { return new Response(null); } }",
|
||||||
|
kvNamespaces: ["TEST"],
|
||||||
|
});
|
||||||
|
const kv = await mf.getKVNamespace("TEST");
|
||||||
|
return new CloudflareKVCachePool(kv as any);
|
||||||
|
},
|
||||||
|
createItem: (key, value) => new CloudflareKVCacheItem(key, value),
|
||||||
|
tester: {
|
||||||
|
test,
|
||||||
|
beforeEach,
|
||||||
|
expect: (actual?: any) => {
|
||||||
|
return {
|
||||||
|
toBe(expected: any) {
|
||||||
|
assert.equal(actual, expected);
|
||||||
|
},
|
||||||
|
toEqual(expected: any) {
|
||||||
|
assert.deepEqual(actual, expected);
|
||||||
|
},
|
||||||
|
toBeUndefined() {
|
||||||
|
assert.equal(actual, undefined);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await mf?.dispose();
|
||||||
|
});
|
||||||
|
});
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
import { beforeEach, describe, expect, test } from "bun:test";
|
||||||
|
import { MemoryCache, MemoryCacheItem } from "../../../src/core/cache/adapters/MemoryCache";
|
||||||
|
import { runTests } from "./cache-test-suite";
|
||||||
|
|
||||||
|
describe("MemoryCache", () => {
|
||||||
|
runTests({
|
||||||
|
createCache: async () => new MemoryCache(),
|
||||||
|
createItem: (key, value) => new MemoryCacheItem(key, value),
|
||||||
|
tester: {
|
||||||
|
test,
|
||||||
|
beforeEach,
|
||||||
|
expect,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
//import { beforeEach as bunBeforeEach, expect as bunExpect, test as bunTest } from "bun:test";
|
||||||
|
import type { ICacheItem, ICachePool } from "../../../src/core/cache/cache-interface";
|
||||||
|
|
||||||
|
export type TestOptions = {
|
||||||
|
createCache: () => Promise<ICachePool>;
|
||||||
|
createItem: (key: string, value: any) => ICacheItem;
|
||||||
|
tester: {
|
||||||
|
test: (name: string, fn: () => Promise<void>) => void;
|
||||||
|
beforeEach: (fn: () => Promise<void>) => void;
|
||||||
|
expect: (actual?: any) => {
|
||||||
|
toBe(expected: any): void;
|
||||||
|
toEqual(expected: any): void;
|
||||||
|
toBeUndefined(): void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function runTests({ createCache, createItem, tester }: TestOptions) {
|
||||||
|
let cache: ICachePool<string>;
|
||||||
|
const { test, beforeEach, expect } = tester;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
cache = await createCache();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getItem returns correct item", async () => {
|
||||||
|
const item = createItem("key1", "value1");
|
||||||
|
await cache.save(item);
|
||||||
|
const retrievedItem = await cache.get("key1");
|
||||||
|
expect(retrievedItem.value()).toEqual(item.value());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getItem returns new item when key does not exist", async () => {
|
||||||
|
const retrievedItem = await cache.get("key1");
|
||||||
|
expect(retrievedItem.key()).toEqual("key1");
|
||||||
|
expect(retrievedItem.value()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getItems returns correct items", async () => {
|
||||||
|
const item1 = createItem("key1", "value1");
|
||||||
|
const item2 = createItem("key2", "value2");
|
||||||
|
await cache.save(item1);
|
||||||
|
await cache.save(item2);
|
||||||
|
const retrievedItems = await cache.getMany(["key1", "key2"]);
|
||||||
|
expect(retrievedItems.get("key1")?.value()).toEqual(item1.value());
|
||||||
|
expect(retrievedItems.get("key2")?.value()).toEqual(item2.value());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("hasItem returns true when item exists and is a hit", async () => {
|
||||||
|
const item = createItem("key1", "value1");
|
||||||
|
await cache.save(item);
|
||||||
|
expect(await cache.has("key1")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("clear and deleteItem correctly clear the cache and delete items", async () => {
|
||||||
|
const item = createItem("key1", "value1");
|
||||||
|
await cache.save(item);
|
||||||
|
|
||||||
|
if (cache.supports().clear) {
|
||||||
|
await cache.clear();
|
||||||
|
} else {
|
||||||
|
await cache.delete("key1");
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(await cache.has("key1")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("save correctly saves items to the cache", async () => {
|
||||||
|
const item = createItem("key1", "value1");
|
||||||
|
await cache.save(item);
|
||||||
|
expect(await cache.has("key1")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("putItem correctly puts items in the cache ", async () => {
|
||||||
|
await cache.put("key1", "value1", { ttl: 60 });
|
||||||
|
const item = await cache.get("key1");
|
||||||
|
expect(item.value()).toEqual("value1");
|
||||||
|
expect(item.hit()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*test("commit returns true", async () => {
|
||||||
|
expect(await cache.commit()).toBe(true);
|
||||||
|
});*/
|
||||||
|
}
|
||||||
@@ -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 "../../../src/core/utils";
|
||||||
|
|
||||||
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: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { type ObjectQuery, convert, validate } from "core/object/query/object-query";
|
import { type ObjectQuery, convert, validate } from "../../../src/core/object/query/object-query";
|
||||||
|
|
||||||
describe("object-query", () => {
|
describe("object-query", () => {
|
||||||
const q: ObjectQuery = { name: "Michael" };
|
const q: ObjectQuery = { name: "Michael" };
|
||||||
@@ -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" } });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
+55
-280
@@ -1,7 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Perf, ucFirst } from "../../src/core/utils";
|
import { Perf, datetimeStringUTC, isBlob, ucFirst } from "../../src/core/utils";
|
||||||
import * as utils from "../../src/core/utils";
|
import * as utils from "../../src/core/utils";
|
||||||
import { assetsPath } from "../helper";
|
|
||||||
|
|
||||||
async function wait(ms: number) {
|
async function wait(ms: number) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@@ -76,6 +75,57 @@ describe("Core Utils", async () => {
|
|||||||
const result3 = utils.encodeSearch(obj3, { encode: true });
|
const result3 = utils.encodeSearch(obj3, { encode: true });
|
||||||
expect(result3).toBe("id=123&name=%7B%22test%22%3A%22test%22%7D");
|
expect(result3).toBe("id=123&name=%7B%22test%22%3A%22test%22%7D");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("guards", () => {
|
||||||
|
const types = {
|
||||||
|
blob: new Blob(),
|
||||||
|
file: new File([""], "file.txt"),
|
||||||
|
stream: new ReadableStream(),
|
||||||
|
arrayBuffer: new ArrayBuffer(10),
|
||||||
|
arrayBufferView: new Uint8Array(new ArrayBuffer(10)),
|
||||||
|
};
|
||||||
|
|
||||||
|
const fns = [
|
||||||
|
[utils.isReadableStream, "stream"],
|
||||||
|
[utils.isBlob, "blob", ["stream", "arrayBuffer", "arrayBufferView"]],
|
||||||
|
[utils.isFile, "file", ["stream", "arrayBuffer", "arrayBufferView"]],
|
||||||
|
[utils.isArrayBuffer, "arrayBuffer"],
|
||||||
|
[utils.isArrayBufferView, "arrayBufferView"],
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const additional = [0, 0.0, "", null, undefined, {}, []];
|
||||||
|
|
||||||
|
for (const [fn, type, _to_test] of fns) {
|
||||||
|
test(`is${ucFirst(type)}`, () => {
|
||||||
|
const to_test = _to_test ?? (Object.keys(types) as string[]);
|
||||||
|
for (const key of to_test) {
|
||||||
|
const value = types[key as keyof typeof types];
|
||||||
|
const result = fn(value);
|
||||||
|
expect(result).toBe(key === type);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const value of additional) {
|
||||||
|
const result = fn(value);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getContentName", () => {
|
||||||
|
const name = "test.json";
|
||||||
|
const text = "attachment; filename=" + name;
|
||||||
|
const headers = new Headers({
|
||||||
|
"Content-Disposition": text,
|
||||||
|
});
|
||||||
|
const request = new Request("http://example.com", {
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(utils.getContentName(text)).toBe(name);
|
||||||
|
expect(utils.getContentName(headers)).toBe(name);
|
||||||
|
expect(utils.getContentName(request)).toBe(name);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("perf", async () => {
|
describe("perf", async () => {
|
||||||
@@ -194,290 +244,15 @@ 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("type guards", () => {
|
|
||||||
const types = {
|
|
||||||
blob: new Blob(),
|
|
||||||
file: new File([""], "file.txt"),
|
|
||||||
stream: new ReadableStream(),
|
|
||||||
arrayBuffer: new ArrayBuffer(10),
|
|
||||||
arrayBufferView: new Uint8Array(new ArrayBuffer(10)),
|
|
||||||
};
|
|
||||||
|
|
||||||
const fns = [
|
|
||||||
[utils.isReadableStream, "stream"],
|
|
||||||
[utils.isBlob, "blob", ["stream", "arrayBuffer", "arrayBufferView"]],
|
|
||||||
[utils.isFile, "file", ["stream", "arrayBuffer", "arrayBufferView"]],
|
|
||||||
[utils.isArrayBuffer, "arrayBuffer"],
|
|
||||||
[utils.isArrayBufferView, "arrayBufferView"],
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const additional = [0, 0.0, "", null, undefined, {}, []];
|
|
||||||
|
|
||||||
for (const [fn, type, _to_test] of fns) {
|
|
||||||
test(`is${ucFirst(type)}`, () => {
|
|
||||||
const to_test = _to_test ?? (Object.keys(types) as string[]);
|
|
||||||
for (const key of to_test) {
|
|
||||||
const value = types[key as keyof typeof types];
|
|
||||||
const result = fn(value);
|
|
||||||
expect(result).toBe(key === type);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const value of additional) {
|
|
||||||
const result = fn(value);
|
|
||||||
expect(result).toBe(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("getContentName", () => {
|
|
||||||
const name = "test.json";
|
|
||||||
const text = "attachment; filename=" + name;
|
|
||||||
const headers = new Headers({
|
|
||||||
"Content-Disposition": text,
|
|
||||||
});
|
|
||||||
const request = new Request("http://example.com", {
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(utils.getContentName(text)).toBe(name);
|
|
||||||
expect(utils.getContentName(headers)).toBe(name);
|
|
||||||
expect(utils.getContentName(request)).toBe(name);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("detectImageDimensions", async () => {
|
|
||||||
// wrong
|
|
||||||
// @ts-expect-error
|
|
||||||
expect(utils.detectImageDimensions(new ArrayBuffer(), "text/plain")).rejects.toThrow();
|
|
||||||
|
|
||||||
// successful ones
|
|
||||||
const getFile = (name: string): File => Bun.file(`${assetsPath}/${name}`) as any;
|
|
||||||
expect(await utils.detectImageDimensions(getFile("image.png"))).toEqual({
|
|
||||||
width: 362,
|
|
||||||
height: 387,
|
|
||||||
});
|
|
||||||
expect(await utils.detectImageDimensions(getFile("image.jpg"))).toEqual({
|
|
||||||
width: 453,
|
|
||||||
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+80
-13
@@ -1,18 +1,25 @@
|
|||||||
import { describe, test, expect } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { getDummyConnection } from "../helper";
|
import { Value, _jsonp } from "../../src/core/utils";
|
||||||
import { WhereBuilder, type WhereQuery } from "data/entities/query/WhereBuilder";
|
import { type RepoQuery, WhereBuilder, type WhereQuery, querySchema } from "../../src/data";
|
||||||
|
import type { RepoQueryIn } from "../../src/data/server/data-query-impl";
|
||||||
|
import { getDummyConnection } from "./helper";
|
||||||
|
|
||||||
function qb() {
|
const decode = (input: RepoQueryIn, expected: RepoQuery) => {
|
||||||
const c = getDummyConnection();
|
const result = Value.Decode(querySchema, input);
|
||||||
const kysely = c.dummyConnection.kysely;
|
expect(result).toEqual(expected);
|
||||||
return kysely.selectFrom("t").selectAll();
|
};
|
||||||
}
|
|
||||||
function compile(q: WhereQuery) {
|
describe("data-query-impl", () => {
|
||||||
const { sql, parameters } = WhereBuilder.addClause(qb(), q).compile();
|
function qb() {
|
||||||
return { sql, parameters };
|
const c = getDummyConnection();
|
||||||
}
|
const kysely = c.dummyConnection.kysely;
|
||||||
|
return kysely.selectFrom("t").selectAll();
|
||||||
|
}
|
||||||
|
function compile(q: WhereQuery) {
|
||||||
|
const { sql, parameters } = WhereBuilder.addClause(qb(), q).compile();
|
||||||
|
return { sql, parameters };
|
||||||
|
}
|
||||||
|
|
||||||
describe("WhereBuilder", () => {
|
|
||||||
test("single validation", () => {
|
test("single validation", () => {
|
||||||
const tests: [WhereQuery, string, any[]][] = [
|
const tests: [WhereQuery, string, any[]][] = [
|
||||||
[{ name: "Michael", age: 40 }, '("name" = ? and "age" = ?)', ["Michael", 40]],
|
[{ name: "Michael", age: 40 }, '("name" = ? and "age" = ?)', ["Michael", 40]],
|
||||||
@@ -87,4 +94,64 @@ describe("WhereBuilder", () => {
|
|||||||
expect(keys).toEqual(expectedKeys);
|
expect(keys).toEqual(expectedKeys);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("with", () => {
|
||||||
|
decode({ with: ["posts"] }, { with: { posts: {} } });
|
||||||
|
decode({ with: { posts: {} } }, { with: { posts: {} } });
|
||||||
|
decode({ with: { posts: { limit: 1 } } }, { with: { posts: { limit: 1 } } });
|
||||||
|
decode(
|
||||||
|
{
|
||||||
|
with: {
|
||||||
|
posts: {
|
||||||
|
with: {
|
||||||
|
images: {
|
||||||
|
select: ["id"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
with: {
|
||||||
|
posts: {
|
||||||
|
with: {
|
||||||
|
images: {
|
||||||
|
select: ["id"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// over http
|
||||||
|
{
|
||||||
|
const output = { with: { images: {} } };
|
||||||
|
decode({ with: "images" }, output);
|
||||||
|
decode({ with: '["images"]' }, output);
|
||||||
|
decode({ with: ["images"] }, output);
|
||||||
|
decode({ with: { images: {} } }, output);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const output = { with: { images: {}, comments: {} } };
|
||||||
|
decode({ with: "images,comments" }, output);
|
||||||
|
decode({ with: ["images", "comments"] }, output);
|
||||||
|
decode({ with: '["images", "comments"]' }, output);
|
||||||
|
decode({ with: { images: {}, comments: {} } }, output);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("data-query-impl: Typebox", () => {
|
||||||
|
test("sort", async () => {
|
||||||
|
const _dflt = { sort: { by: "id", dir: "asc" } };
|
||||||
|
|
||||||
|
decode({ sort: "" }, _dflt);
|
||||||
|
decode({ sort: "name" }, { sort: { by: "name", dir: "asc" } });
|
||||||
|
decode({ sort: "-name" }, { sort: { by: "name", dir: "desc" } });
|
||||||
|
decode({ sort: "-posts.name" }, { sort: { by: "posts.name", dir: "desc" } });
|
||||||
|
decode({ sort: "-1name" }, _dflt);
|
||||||
|
decode({ sort: { by: "name", dir: "desc" } }, { sort: { by: "name", dir: "desc" } });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -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 () => {
|
||||||
@@ -97,18 +110,4 @@ describe("some tests", async () => {
|
|||||||
new EntityManager([entity, entity2], connection);
|
new EntityManager([entity, entity2], connection);
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("primary uuid", async () => {
|
|
||||||
const entity = new Entity("users", [
|
|
||||||
new PrimaryField("id", { format: "uuid" }),
|
|
||||||
new TextField("username"),
|
|
||||||
]);
|
|
||||||
const em = new EntityManager([entity], getDummyConnection().dummyConnection);
|
|
||||||
await em.schema().sync({ force: true });
|
|
||||||
|
|
||||||
const mutator = em.mutator(entity);
|
|
||||||
const data = await mutator.insertOne({ username: "test" });
|
|
||||||
expect(data.data.id).toBeDefined();
|
|
||||||
expect(data.data.id).toBeString();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,5 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Entity } from "data/entities";
|
import { Entity, NumberField, TextField } from "../../../src/data";
|
||||||
import { NumberField, TextField } from "data/fields";
|
|
||||||
|
|
||||||
describe("[data] Entity", async () => {
|
describe("[data] Entity", async () => {
|
||||||
const entity = new Entity("test", [
|
const entity = new Entity("test", [
|
||||||
@@ -47,4 +46,15 @@ describe("[data] Entity", async () => {
|
|||||||
entity.addField(field);
|
entity.addField(field);
|
||||||
expect(entity.getField("new_field")).toBe(field);
|
expect(entity.getField("new_field")).toBe(field);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// @todo: move this to ClientApp
|
||||||
|
/*test("serialize and deserialize", async () => {
|
||||||
|
const json = entity.toJSON();
|
||||||
|
//sconsole.log("json", json.fields);
|
||||||
|
const newEntity = Entity.deserialize(json);
|
||||||
|
//console.log("newEntity", newEntity.toJSON().fields);
|
||||||
|
expect(newEntity).toBeInstanceOf(Entity);
|
||||||
|
expect(json).toEqual(newEntity.toJSON());
|
||||||
|
expect(json.fields).toEqual(newEntity.toJSON().fields);
|
||||||
|
});*/
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -43,8 +47,8 @@ describe("[data] EntityManager", async () => {
|
|||||||
em.addRelation(new ManyToOneRelation(posts, users));
|
em.addRelation(new ManyToOneRelation(posts, users));
|
||||||
expect(em.relations.all.length).toBe(1);
|
expect(em.relations.all.length).toBe(1);
|
||||||
expect(em.relations.all[0]).toBeInstanceOf(ManyToOneRelation);
|
expect(em.relations.all[0]).toBeInstanceOf(ManyToOneRelation);
|
||||||
expect(em.relationsOf("users")).toEqual([em.relations.all[0]!]);
|
expect(em.relationsOf("users")).toEqual([em.relations.all[0]]);
|
||||||
expect(em.relationsOf("posts")).toEqual([em.relations.all[0]!]);
|
expect(em.relationsOf("posts")).toEqual([em.relations.all[0]]);
|
||||||
expect(em.hasRelations("users")).toBe(true);
|
expect(em.hasRelations("users")).toBe(true);
|
||||||
expect(em.hasRelations("posts")).toBe(true);
|
expect(em.hasRelations("posts")).toBe(true);
|
||||||
expect(em.relatedEntitiesOf("users")).toEqual([posts]);
|
expect(em.relatedEntitiesOf("users")).toEqual([posts]);
|
||||||
|
|||||||
@@ -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 }),
|
||||||
@@ -290,17 +288,14 @@ describe("[data] Mutator (Events)", async () => {
|
|||||||
|
|
||||||
test("events were fired", async () => {
|
test("events were fired", async () => {
|
||||||
const { data } = await mutator.insertOne({ label: "test" });
|
const { data } = await mutator.insertOne({ label: "test" });
|
||||||
await mutator.emgr.executeAsyncs();
|
|
||||||
expect(events.has(MutatorEvents.MutatorInsertBefore.slug)).toBeTrue();
|
expect(events.has(MutatorEvents.MutatorInsertBefore.slug)).toBeTrue();
|
||||||
expect(events.has(MutatorEvents.MutatorInsertAfter.slug)).toBeTrue();
|
expect(events.has(MutatorEvents.MutatorInsertAfter.slug)).toBeTrue();
|
||||||
|
|
||||||
await mutator.updateOne(data.id, { label: "test2" });
|
await mutator.updateOne(data.id, { label: "test2" });
|
||||||
await mutator.emgr.executeAsyncs();
|
|
||||||
expect(events.has(MutatorEvents.MutatorUpdateBefore.slug)).toBeTrue();
|
expect(events.has(MutatorEvents.MutatorUpdateBefore.slug)).toBeTrue();
|
||||||
expect(events.has(MutatorEvents.MutatorUpdateAfter.slug)).toBeTrue();
|
expect(events.has(MutatorEvents.MutatorUpdateAfter.slug)).toBeTrue();
|
||||||
|
|
||||||
await mutator.deleteOne(data.id);
|
await mutator.deleteOne(data.id);
|
||||||
await mutator.emgr.executeAsyncs();
|
|
||||||
expect(events.has(MutatorEvents.MutatorDeleteBefore.slug)).toBeTrue();
|
expect(events.has(MutatorEvents.MutatorDeleteBefore.slug)).toBeTrue();
|
||||||
expect(events.has(MutatorEvents.MutatorDeleteAfter.slug)).toBeTrue();
|
expect(events.has(MutatorEvents.MutatorDeleteAfter.slug)).toBeTrue();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
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 "../../../src/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,
|
||||||
|
} from "../../../src/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 +23,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,88 +157,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 () => {
|
|
||||||
const em = $em({
|
|
||||||
items: $entity("items", {
|
|
||||||
label: $text(),
|
|
||||||
}),
|
|
||||||
}).proto.withConnection(getDummyConnection().dummyConnection);
|
|
||||||
|
|
||||||
// should throw because table doesn't exist
|
|
||||||
expect(em.repo("items").findMany({})).rejects.toThrow(/no such table/);
|
|
||||||
// should silently return empty result
|
|
||||||
em.repo("items", { silent: true })
|
|
||||||
.findMany({})
|
|
||||||
.then((r) => r.data);
|
|
||||||
expect(
|
|
||||||
em
|
|
||||||
.repo("items", { silent: true })
|
|
||||||
.findMany({})
|
|
||||||
.then((r) => r.data),
|
|
||||||
).resolves.toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("option: includeCounts", async () => {
|
|
||||||
const em = $em({
|
|
||||||
items: $entity("items", {
|
|
||||||
label: $text(),
|
|
||||||
}),
|
|
||||||
}).proto.withConnection(getDummyConnection().dummyConnection);
|
|
||||||
await em.schema().sync({ force: true });
|
|
||||||
|
|
||||||
expect(
|
|
||||||
em
|
|
||||||
.repo("items", { includeCounts: true })
|
|
||||||
.findMany({})
|
|
||||||
.then((r) => [r.count, r.total]),
|
|
||||||
).resolves.toEqual([0, 0]);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
em
|
|
||||||
.repo("items", { includeCounts: false })
|
|
||||||
.findMany({})
|
|
||||||
.then((r) => [r.count, r.total]),
|
|
||||||
).resolves.toEqual([undefined, undefined]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -145,36 +198,24 @@ describe("[data] Repository (Events)", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("events were fired", async () => {
|
test("events were fired", async () => {
|
||||||
const repo = em.repository(items);
|
await em.repository(items).findId(1);
|
||||||
await repo.findId(1);
|
|
||||||
await repo.emgr.executeAsyncs();
|
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue();
|
expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue();
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
|
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
|
||||||
events.clear();
|
events.clear();
|
||||||
|
|
||||||
await repo.findOne({ id: 1 });
|
await em.repository(items).findOne({ id: 1 });
|
||||||
await repo.emgr.executeAsyncs();
|
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue();
|
expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue();
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
|
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
|
||||||
events.clear();
|
events.clear();
|
||||||
|
|
||||||
await repo.findMany({ where: { id: 1 } });
|
await em.repository(items).findMany({ where: { id: 1 } });
|
||||||
await repo.emgr.executeAsyncs();
|
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindManyBefore.slug)).toBeTrue();
|
expect(events.has(RepositoryEvents.RepositoryFindManyBefore.slug)).toBeTrue();
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue();
|
expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue();
|
||||||
events.clear();
|
events.clear();
|
||||||
|
|
||||||
await repo.findManyByReference(1, "categories");
|
await em.repository(items).findManyByReference(1, "categories");
|
||||||
await repo.emgr.executeAsyncs();
|
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindManyBefore.slug)).toBeTrue();
|
expect(events.has(RepositoryEvents.RepositoryFindManyBefore.slug)).toBeTrue();
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue();
|
expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue();
|
||||||
events.clear();
|
events.clear();
|
||||||
|
|
||||||
// check find one on findMany with limit 1
|
|
||||||
await repo.findMany({ where: { id: 1 }, limit: 1 });
|
|
||||||
await repo.emgr.executeAsyncs();
|
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue();
|
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
|
|
||||||
events.clear();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,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 { runBaseFieldTests, transformPersist } from "./inc";
|
||||||
|
|
||||||
describe("[data] BooleanField", async () => {
|
describe("[data] BooleanField", async () => {
|
||||||
fieldTestSuite(bunTestRunner, BooleanField, { defaultValue: true, schemaType: "boolean" });
|
runBaseFieldTests(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 { runBaseFieldTests } from "./inc";
|
||||||
import { bunTestRunner } from "adapter/bun/test";
|
|
||||||
|
|
||||||
describe("[data] DateField", async () => {
|
describe("[data] DateField", async () => {
|
||||||
fieldTestSuite(
|
runBaseFieldTests(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,16 +1,13 @@
|
|||||||
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 { runBaseFieldTests, transformPersist } from "./inc";
|
||||||
|
|
||||||
function options(strings: string[]) {
|
function options(strings: string[]) {
|
||||||
return { type: "strings", values: strings };
|
return { type: "strings", values: strings };
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("[data] EnumField", async () => {
|
describe("[data] EnumField", async () => {
|
||||||
fieldTestSuite(
|
runBaseFieldTests(
|
||||||
bunTestRunner,
|
|
||||||
// @ts-ignore
|
|
||||||
EnumField,
|
EnumField,
|
||||||
{ defaultValue: "a", schemaType: "text" },
|
{ defaultValue: "a", schemaType: "text" },
|
||||||
{ options: options(["a", "b", "c"]) },
|
{ options: options(["a", "b", "c"]) },
|
||||||
@@ -18,13 +15,11 @@ describe("[data] EnumField", async () => {
|
|||||||
|
|
||||||
test("yields if default value is not a valid option", async () => {
|
test("yields if default value is not a valid option", async () => {
|
||||||
expect(
|
expect(
|
||||||
// @ts-ignore
|
|
||||||
() => new EnumField("test", { options: options(["a", "b"]), default_value: "c" }),
|
() => new EnumField("test", { options: options(["a", "b"]), default_value: "c" }),
|
||||||
).toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("transformPersist (config)", async () => {
|
test("transformPersist (config)", async () => {
|
||||||
// @ts-ignore
|
|
||||||
const field = new EnumField("test", { options: options(["a", "b", "c"]) });
|
const field = new EnumField("test", { options: options(["a", "b", "c"]) });
|
||||||
|
|
||||||
expect(transformPersist(field, null)).resolves.toBeUndefined();
|
expect(transformPersist(field, null)).resolves.toBeUndefined();
|
||||||
@@ -34,7 +29,6 @@ describe("[data] EnumField", async () => {
|
|||||||
|
|
||||||
test("transformRetrieve", async () => {
|
test("transformRetrieve", async () => {
|
||||||
const field = new EnumField("test", {
|
const field = new EnumField("test", {
|
||||||
// @ts-ignore
|
|
||||||
options: options(["a", "b", "c"]),
|
options: options(["a", "b", "c"]),
|
||||||
default_value: "a",
|
default_value: "a",
|
||||||
required: true,
|
required: true,
|
||||||
|
|||||||
@@ -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 { runBaseFieldTests } from "./inc";
|
||||||
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" });
|
runBaseFieldTests(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,14 +1,19 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Entity } from "data/entities";
|
import { Type } from "../../../../src/core/utils";
|
||||||
import { Field, EntityIndex } from "data/fields";
|
import {
|
||||||
import { s } from "core/utils/schema";
|
Entity,
|
||||||
|
EntityIndex,
|
||||||
|
type EntityManager,
|
||||||
|
Field,
|
||||||
|
type SchemaResponse,
|
||||||
|
} from "../../../../src/data";
|
||||||
|
|
||||||
class TestField extends Field {
|
class TestField extends Field {
|
||||||
protected getSchema(): any {
|
protected getSchema(): any {
|
||||||
return s.any();
|
return Type.Any();
|
||||||
}
|
}
|
||||||
|
|
||||||
override schema() {
|
schema(em: EntityManager<any>): SchemaResponse {
|
||||||
return undefined as any;
|
return undefined as any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,4 +28,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 { runBaseFieldTests, transformPersist } from "./inc";
|
||||||
|
|
||||||
describe("[data] JsonField", async () => {
|
describe("[data] JsonField", async () => {
|
||||||
const field = new JsonField("test");
|
const field = new JsonField("test");
|
||||||
fieldTestSuite(bunTestRunner, JsonField, {
|
runBaseFieldTests(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,10 +1,9 @@
|
|||||||
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 { runBaseFieldTests } from "./inc";
|
||||||
|
|
||||||
describe("[data] JsonSchemaField", async () => {
|
describe("[data] JsonSchemaField", async () => {
|
||||||
// @ts-ignore
|
runBaseFieldTests(JsonSchemaField, { defaultValue: {}, schemaType: "text" });
|
||||||
fieldTestSuite({ expect, test }, JsonSchemaField, { defaultValue: {}, schemaType: "text" });
|
|
||||||
|
|
||||||
// @todo: add JsonSchemaField tests
|
// @todo: add JsonSchemaField tests
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { bunTestRunner } from "adapter/bun/test";
|
|
||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { NumberField } from "data/fields";
|
import { NumberField } from "../../../../src/data";
|
||||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
import { runBaseFieldTests, transformPersist } from "./inc";
|
||||||
|
|
||||||
describe("[data] NumberField", async () => {
|
describe("[data] NumberField", async () => {
|
||||||
test("transformPersist (config)", async () => {
|
test("transformPersist (config)", 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" });
|
runBaseFieldTests(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");
|
||||||
@@ -39,28 +39,4 @@ describe("[data] PrimaryField", async () => {
|
|||||||
expect(field.transformPersist(1)).rejects.toThrow();
|
expect(field.transformPersist(1)).rejects.toThrow();
|
||||||
expect(field.transformRetrieve(1)).toBe(1);
|
expect(field.transformRetrieve(1)).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("format", () => {
|
|
||||||
const uuid = new PrimaryField("uuid", { format: "uuid" });
|
|
||||||
expect(uuid.format).toBe("uuid");
|
|
||||||
expect(uuid.fieldType).toBe("text");
|
|
||||||
expect(uuid.getNewValue()).toBeString();
|
|
||||||
expect(uuid.toType()).toEqual({
|
|
||||||
required: true,
|
|
||||||
comment: undefined,
|
|
||||||
type: "Generated<string>",
|
|
||||||
import: [{ package: "kysely", name: "Generated" }],
|
|
||||||
});
|
|
||||||
|
|
||||||
const integer = new PrimaryField("integer", { format: "integer" });
|
|
||||||
expect(integer.format).toBe("integer");
|
|
||||||
expect(integer.fieldType).toBe("integer");
|
|
||||||
expect(integer.getNewValue()).toBeUndefined();
|
|
||||||
expect(integer.toType()).toEqual({
|
|
||||||
required: true,
|
|
||||||
comment: undefined,
|
|
||||||
type: "Generated<number>",
|
|
||||||
import: [{ package: "kysely", name: "Generated" }],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 { runBaseFieldTests, transformPersist } from "./inc";
|
||||||
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" });
|
runBaseFieldTests(TextField, { defaultValue: "abc", schemaType: "text" });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { BaseFieldConfig, Field, TActionContext } from "data/fields";
|
import { expect, test } from "bun:test";
|
||||||
import type { ColumnDataType } from "kysely";
|
import type { ColumnDataType } from "kysely";
|
||||||
import { omit } from "lodash-es";
|
import { omit } from "lodash-es";
|
||||||
import type { TestRunner } from "core/test";
|
import type { BaseFieldConfig, Field, TActionContext } from "../../../../src/data";
|
||||||
|
|
||||||
type ConstructableField = new (name: string, config?: Partial<BaseFieldConfig>) => Field;
|
type ConstructableField = new (name: string, config?: Partial<BaseFieldConfig>) => Field;
|
||||||
|
|
||||||
@@ -15,13 +15,11 @@ export function transformPersist(field: Field, value: any, context?: TActionCont
|
|||||||
return field.transformPersist(value, undefined as any, context as any);
|
return field.transformPersist(value, undefined as any, context as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fieldTestSuite(
|
export function runBaseFieldTests(
|
||||||
testRunner: TestRunner,
|
|
||||||
fieldClass: ConstructableField,
|
fieldClass: ConstructableField,
|
||||||
config: FieldTestConfig,
|
config: FieldTestConfig,
|
||||||
_requiredConfig: any = {},
|
_requiredConfig: any = {},
|
||||||
) {
|
) {
|
||||||
const { test, expect } = testRunner;
|
|
||||||
const noConfigField = new fieldClass("no_config", _requiredConfig);
|
const noConfigField = new fieldClass("no_config", _requiredConfig);
|
||||||
const fillable = new fieldClass("fillable", { ..._requiredConfig, fillable: true });
|
const fillable = new fieldClass("fillable", { ..._requiredConfig, fillable: true });
|
||||||
const required = new fieldClass("required", { ..._requiredConfig, required: true });
|
const required = new fieldClass("required", { ..._requiredConfig, required: true });
|
||||||
@@ -50,7 +48,7 @@ export function fieldTestSuite(
|
|||||||
expect(noConfigField.hasDefault()).toBe(false);
|
expect(noConfigField.hasDefault()).toBe(false);
|
||||||
expect(noConfigField.getDefault()).toBeUndefined();
|
expect(noConfigField.getDefault()).toBeUndefined();
|
||||||
expect(dflt.hasDefault()).toBe(true);
|
expect(dflt.hasDefault()).toBe(true);
|
||||||
expect(dflt.getDefault()).toEqual(config.defaultValue);
|
expect(dflt.getDefault()).toBe(config.defaultValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("isFillable", async () => {
|
test("isFillable", async () => {
|
||||||
@@ -78,9 +76,9 @@ export function fieldTestSuite(
|
|||||||
const isPrimitive = (v) => ["string", "number"].includes(typeof v);
|
const isPrimitive = (v) => ["string", "number"].includes(typeof v);
|
||||||
for (const value of config.sampleValues!) {
|
for (const value of config.sampleValues!) {
|
||||||
// "form"
|
// "form"
|
||||||
expect(isPrimitive(noConfigField.getValue(value, "form"))).toBe(true);
|
expect(isPrimitive(noConfigField.getValue(value, "form"))).toBeTrue();
|
||||||
// "table"
|
// "table"
|
||||||
expect(isPrimitive(noConfigField.getValue(value, "table"))).toBe(true);
|
expect(isPrimitive(noConfigField.getValue(value, "table"))).toBeTrue();
|
||||||
// "read"
|
// "read"
|
||||||
// "submit"
|
// "submit"
|
||||||
}
|
}
|
||||||
@@ -98,7 +96,12 @@ export function fieldTestSuite(
|
|||||||
test("toJSON", async () => {
|
test("toJSON", async () => {
|
||||||
const _config = {
|
const _config = {
|
||||||
..._requiredConfig,
|
..._requiredConfig,
|
||||||
|
//order: 1,
|
||||||
|
fillable: true,
|
||||||
required: false,
|
required: false,
|
||||||
|
hidden: false,
|
||||||
|
//virtual: false,
|
||||||
|
//default_value: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
function fieldJson(field: Field) {
|
function fieldJson(field: Field) {
|
||||||
@@ -110,19 +113,19 @@ export function fieldTestSuite(
|
|||||||
}
|
}
|
||||||
|
|
||||||
expect(fieldJson(noConfigField)).toEqual({
|
expect(fieldJson(noConfigField)).toEqual({
|
||||||
|
//name: "no_config",
|
||||||
type: noConfigField.type,
|
type: noConfigField.type,
|
||||||
config: _config,
|
config: _config,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(fieldJson(fillable)).toEqual({
|
expect(fieldJson(fillable)).toEqual({
|
||||||
|
//name: "fillable",
|
||||||
type: noConfigField.type,
|
type: noConfigField.type,
|
||||||
config: {
|
config: _config,
|
||||||
..._config,
|
|
||||||
fillable: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(fieldJson(required)).toEqual({
|
expect(fieldJson(required)).toEqual({
|
||||||
|
//name: "required",
|
||||||
type: required.type,
|
type: required.type,
|
||||||
config: {
|
config: {
|
||||||
..._config,
|
..._config,
|
||||||
@@ -131,6 +134,7 @@ export function fieldTestSuite(
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(fieldJson(hidden)).toEqual({
|
expect(fieldJson(hidden)).toEqual({
|
||||||
|
//name: "hidden",
|
||||||
type: required.type,
|
type: required.type,
|
||||||
config: {
|
config: {
|
||||||
..._config,
|
..._config,
|
||||||
@@ -139,6 +143,7 @@ export function fieldTestSuite(
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(fieldJson(dflt)).toEqual({
|
expect(fieldJson(dflt)).toEqual({
|
||||||
|
//name: "dflt",
|
||||||
type: dflt.type,
|
type: dflt.type,
|
||||||
config: {
|
config: {
|
||||||
..._config,
|
..._config,
|
||||||
@@ -147,6 +152,7 @@ export function fieldTestSuite(
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(fieldJson(requiredAndDefault)).toEqual({
|
expect(fieldJson(requiredAndDefault)).toEqual({
|
||||||
|
//name: "full",
|
||||||
type: requiredAndDefault.type,
|
type: requiredAndDefault.type,
|
||||||
config: {
|
config: {
|
||||||
..._config,
|
..._config,
|
||||||
@@ -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,23 +1,7 @@
|
|||||||
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, RenderTask, SubFlowTask } from "../../src/flows";
|
||||||
import { s } from "core/utils/schema";
|
|
||||||
|
|
||||||
export class StringifyTask<Output extends string> extends Task<
|
describe("SubFlowTask", async () => {
|
||||||
typeof StringifyTask.schema,
|
|
||||||
Output
|
|
||||||
> {
|
|
||||||
type = "stringify";
|
|
||||||
|
|
||||||
static override schema = s.object({
|
|
||||||
input: s.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
async execute() {
|
|
||||||
return JSON.stringify(this.params.input) as Output;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe.skip("SubFlowTask", async () => {
|
|
||||||
test("Simple Subflow", async () => {
|
test("Simple Subflow", async () => {
|
||||||
const subTask = new RenderTask("render", {
|
const subTask = new RenderTask("render", {
|
||||||
render: "subflow",
|
render: "subflow",
|
||||||
@@ -38,6 +22,8 @@ describe.skip("SubFlowTask", async () => {
|
|||||||
|
|
||||||
const execution = flow.createExecution();
|
const execution = flow.createExecution();
|
||||||
await execution.start();
|
await execution.start();
|
||||||
|
/*console.log(execution.logs);
|
||||||
|
console.log(execution.getResponse());*/
|
||||||
|
|
||||||
expect(execution.getResponse()).toEqual("Subflow output: subflow");
|
expect(execution.getResponse()).toEqual("Subflow output: subflow");
|
||||||
});
|
});
|
||||||
@@ -54,8 +40,8 @@ describe.skip("SubFlowTask", async () => {
|
|||||||
loop: true,
|
loop: true,
|
||||||
input: [1, 2, 3],
|
input: [1, 2, 3],
|
||||||
});
|
});
|
||||||
const task3 = new StringifyTask("stringify", {
|
const task3 = new RenderTask("render2", {
|
||||||
input: "{{ sub.output }}",
|
render: `Subflow output: {{ sub.output | join: ", " }}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const flow = new Flow("test", [task, task2, task3], []);
|
const flow = new Flow("test", [task, task2, task3], []);
|
||||||
@@ -65,6 +51,41 @@ describe.skip("SubFlowTask", async () => {
|
|||||||
const execution = flow.createExecution();
|
const execution = flow.createExecution();
|
||||||
await execution.start();
|
await execution.start();
|
||||||
|
|
||||||
expect(execution.getResponse()).toEqual('"run 1,run 2,run 3"');
|
console.log("errors", execution.getErrors());
|
||||||
|
|
||||||
|
/*console.log(execution.logs);
|
||||||
|
console.log(execution.getResponse());*/
|
||||||
|
|
||||||
|
expect(execution.getResponse()).toEqual("Subflow output: run 1, run 2, run 3");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Simple loop from flow input", async () => {
|
||||||
|
const subTask = new RenderTask("render", {
|
||||||
|
render: "run {{ flow.output }}",
|
||||||
|
});
|
||||||
|
|
||||||
|
const subflow = new Flow("subflow", [subTask]);
|
||||||
|
|
||||||
|
const task = new LogTask("log");
|
||||||
|
const task2 = new SubFlowTask("sub", {
|
||||||
|
flow: subflow,
|
||||||
|
loop: true,
|
||||||
|
input: "{{ flow.output | json }}",
|
||||||
|
});
|
||||||
|
const task3 = new RenderTask("render2", {
|
||||||
|
render: `Subflow output: {{ sub.output | join: ", " }}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const flow = new Flow("test", [task, task2, task3], []);
|
||||||
|
flow.task(task).asInputFor(task2);
|
||||||
|
flow.task(task2).asInputFor(task3);
|
||||||
|
|
||||||
|
const execution = flow.createExecution();
|
||||||
|
await execution.start([4, 5, 6]);
|
||||||
|
|
||||||
|
/*console.log(execution.logs);
|
||||||
|
console.log(execution.getResponse());*/
|
||||||
|
|
||||||
|
expect(execution.getResponse()).toEqual("Subflow output: run 4, run 5, run 6");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { Type } from "../../src/core/utils";
|
||||||
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 }}" },
|
||||||
},
|
},
|
||||||
@@ -51,4 +51,62 @@ describe.skip("Task", async () => {
|
|||||||
|
|
||||||
expect(result.test).toEqual({ key: "path", value: "1/1" });
|
expect(result.test).toEqual({ key: "path", value: "1/1" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("resolveParams: with json", async () => {
|
||||||
|
const result = await Task.resolveParams(
|
||||||
|
Type.Object({
|
||||||
|
test: dynamic(Type.Object({ key: Type.String(), value: Type.String() })),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
test: "{{ some | json }}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
some: {
|
||||||
|
key: "path",
|
||||||
|
value: "1/1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.test).toEqual({ key: "path", value: "1/1" });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveParams: with array", async () => {
|
||||||
|
const result = await Task.resolveParams(
|
||||||
|
Type.Object({
|
||||||
|
test: dynamic(Type.Array(Type.String())),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
test: '{{ "1,2,3" | split: "," | json }}',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.test).toEqual(["1", "2", "3"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveParams: boolean", async () => {
|
||||||
|
const result = await Task.resolveParams(
|
||||||
|
Type.Object({
|
||||||
|
test: dynamic(Type.Boolean()),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
test: "{{ true }}",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.test).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveParams: float", async () => {
|
||||||
|
const result = await Task.resolveParams(
|
||||||
|
Type.Object({
|
||||||
|
test: dynamic(Type.Number(), Number.parseFloat),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
test: "{{ 3.14 }}",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.test).toEqual(3.14);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { Event, EventManager } from "../../src/core/events";
|
import { Event, EventManager } from "../../src/core/events";
|
||||||
import { s, parse } from "core/utils/schema";
|
import { type Static, type StaticDecode, Type, parse } from "../../src/core/utils";
|
||||||
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 +14,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 +43,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;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
// eslint-disable-next-line import/no-unresolved
|
// eslint-disable-next-line import/no-unresolved
|
||||||
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 { type Static, Type, _jsonp, withDisabledConsole } from "../../src/core/utils";
|
||||||
import { s } from "core/utils/schema";
|
|
||||||
import { Condition, ExecutionEvent, FetchTask, Flow, LogTask, Task } from "../../src/flows";
|
import { Condition, ExecutionEvent, FetchTask, Flow, LogTask, Task } from "../../src/flows";
|
||||||
|
|
||||||
/*beforeAll(disableConsoleLog);
|
/*beforeAll(disableConsoleLog);
|
||||||
@@ -11,19 +10,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 +77,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
-5
@@ -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> {
|
||||||
@@ -60,7 +78,6 @@ export const assetsTmpPath = `${import.meta.dir}/_assets/tmp`;
|
|||||||
export async function enableFetchLogging() {
|
export async function enableFetchLogging() {
|
||||||
const originalFetch = global.fetch;
|
const originalFetch = global.fetch;
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
global.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
global.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||||
const response = await originalFetch(input, init);
|
const response = await originalFetch(input, init);
|
||||||
const url = input instanceof URL || typeof input === "string" ? input : input.url;
|
const url = input instanceof URL || typeof input === "string" ? input : input.url;
|
||||||
|
|||||||
@@ -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 "../../src/media/storage/adapters/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,8 +39,11 @@ 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.only("accepts direct", async () => {
|
||||||
const app = await makeApp();
|
const app = await makeApp();
|
||||||
|
|
||||||
const file = Bun.file(path);
|
const file = Bun.file(path);
|
||||||
@@ -54,6 +53,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 +94,4 @@ describe("MediaController", () => {
|
|||||||
expect(res.status).toBe(413);
|
expect(res.status).toBe(413);
|
||||||
expect(await Bun.file(assetsTmpPath + "/" + name).exists()).toBe(false);
|
expect(await Bun.file(assetsTmpPath + "/" + name).exists()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("audio files", async () => {
|
|
||||||
const app = await makeApp();
|
|
||||||
const file = Bun.file(`${assetsPath}/test.mp3`);
|
|
||||||
const name = makeName("mp3");
|
|
||||||
const res = await app.server.request("/api/media/upload/" + name, {
|
|
||||||
method: "POST",
|
|
||||||
body: file,
|
|
||||||
});
|
|
||||||
const result = (await res.json()) as any;
|
|
||||||
expect(result.data.mime_type).toStartWith("audio/mpeg");
|
|
||||||
expect(result.name).toBe(name);
|
|
||||||
|
|
||||||
const destFile = Bun.file(assetsTmpPath + "/" + name);
|
|
||||||
expect(destFile.exists()).resolves.toBe(true);
|
|
||||||
await destFile.delete();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("text files", async () => {
|
|
||||||
const app = await makeApp();
|
|
||||||
const file = Bun.file(`${assetsPath}/test.txt`);
|
|
||||||
const name = makeName("txt");
|
|
||||||
const res = await app.server.request("/api/media/upload/" + name, {
|
|
||||||
method: "POST",
|
|
||||||
body: file,
|
|
||||||
});
|
|
||||||
const result = (await res.json()) as any;
|
|
||||||
expect(result.data.mime_type).toStartWith("text/plain");
|
|
||||||
expect(result.name).toBe(name);
|
|
||||||
|
|
||||||
const destFile = Bun.file(assetsTmpPath + "/" + name);
|
|
||||||
expect(destFile.exists()).resolves.toBe(true);
|
|
||||||
await destFile.delete();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
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, type StorageAdapter } 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";
|
|
||||||
|
|
||||||
class TestAdapter extends StorageAdapter {
|
class TestAdapter implements StorageAdapter {
|
||||||
files: Record<string, FileBody> = {};
|
files: Record<string, FileBody> = {};
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
@@ -62,7 +61,7 @@ describe("Storage", async () => {
|
|||||||
test("uploads a file", async () => {
|
test("uploads a file", async () => {
|
||||||
const {
|
const {
|
||||||
meta: { type, size },
|
meta: { type, size },
|
||||||
} = await storage.uploadFile("hello" as any, "world.txt");
|
} = await storage.uploadFile("hello", "world.txt");
|
||||||
expect({ type, size }).toEqual({ type: "text/plain", size: 0 });
|
expect({ type, size }).toEqual({ type: "text/plain", size: 0 });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -72,7 +71,6 @@ describe("Storage", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("events were fired", async () => {
|
test("events were fired", async () => {
|
||||||
await storage.emgr.executeAsyncs();
|
|
||||||
expect(events.has(StorageEvents.FileUploadedEvent.slug)).toBeTrue();
|
expect(events.has(StorageEvents.FileUploadedEvent.slug)).toBeTrue();
|
||||||
expect(events.has(StorageEvents.FileDeletedEvent.slug)).toBeTrue();
|
expect(events.has(StorageEvents.FileDeletedEvent.slug)).toBeTrue();
|
||||||
// @todo: file access must be tested in controllers
|
// @todo: file access must be tested in controllers
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import * as assert from "node:assert/strict";
|
||||||
|
import { createWriteStream } from "node:fs";
|
||||||
|
import { test } from "node:test";
|
||||||
|
import { Miniflare } from "miniflare";
|
||||||
|
|
||||||
|
// https://github.com/nodejs/node/issues/44372#issuecomment-1736530480
|
||||||
|
console.log = async (message: any) => {
|
||||||
|
const tty = createWriteStream("/dev/tty");
|
||||||
|
const msg = typeof message === "string" ? message : JSON.stringify(message, null, 2);
|
||||||
|
return tty.write(`${msg}\n`);
|
||||||
|
};
|
||||||
|
|
||||||
|
test("what", async () => {
|
||||||
|
const mf = new Miniflare({
|
||||||
|
modules: true,
|
||||||
|
script: "export default { async fetch() { return new Response(null); } }",
|
||||||
|
r2Buckets: ["BUCKET"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const bucket = await mf.getR2Bucket("BUCKET");
|
||||||
|
console.log(await bucket.put("count", "1"));
|
||||||
|
|
||||||
|
const object = await bucket.get("count");
|
||||||
|
if (object) {
|
||||||
|
/*const headers = new Headers();
|
||||||
|
object.writeHttpMetadata(headers);
|
||||||
|
headers.set("etag", object.httpEtag);*/
|
||||||
|
console.log("yo -->", await object.text());
|
||||||
|
|
||||||
|
assert.strictEqual(await object.text(), "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
await mf.dispose();
|
||||||
|
});
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { randomString } from "../../../src/core/utils";
|
||||||
|
import { StorageCloudinaryAdapter } from "../../../src/media";
|
||||||
|
|
||||||
|
import { config } from "dotenv";
|
||||||
|
const dotenvOutput = config({ path: `${import.meta.dir}/../../../.env` });
|
||||||
|
const {
|
||||||
|
CLOUDINARY_CLOUD_NAME,
|
||||||
|
CLOUDINARY_API_KEY,
|
||||||
|
CLOUDINARY_API_SECRET,
|
||||||
|
CLOUDINARY_UPLOAD_PRESET,
|
||||||
|
} = dotenvOutput.parsed!;
|
||||||
|
|
||||||
|
const ALL_TESTS = !!process.env.ALL_TESTS;
|
||||||
|
|
||||||
|
describe.skipIf(ALL_TESTS)("StorageCloudinaryAdapter", () => {
|
||||||
|
if (ALL_TESTS) return;
|
||||||
|
|
||||||
|
const adapter = new StorageCloudinaryAdapter({
|
||||||
|
cloud_name: CLOUDINARY_CLOUD_NAME as string,
|
||||||
|
api_key: CLOUDINARY_API_KEY as string,
|
||||||
|
api_secret: CLOUDINARY_API_SECRET as string,
|
||||||
|
upload_preset: CLOUDINARY_UPLOAD_PRESET as string,
|
||||||
|
});
|
||||||
|
|
||||||
|
const file = Bun.file(`${import.meta.dir}/icon.png`);
|
||||||
|
const _filename = randomString(10);
|
||||||
|
const filename = `${_filename}.png`;
|
||||||
|
|
||||||
|
test("object exists", async () => {
|
||||||
|
expect(await adapter.objectExists("7fCTBi6L8c.png")).toBeTrue();
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("puts object", async () => {
|
||||||
|
expect(await adapter.objectExists(filename)).toBeFalse();
|
||||||
|
|
||||||
|
const result = await adapter.putObject(filename, file);
|
||||||
|
console.log("result", result);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result?.name).toBe(filename);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("object exists", async () => {
|
||||||
|
await Bun.sleep(10000);
|
||||||
|
const one = await adapter.objectExists(_filename);
|
||||||
|
const two = await adapter.objectExists(filename);
|
||||||
|
expect(await adapter.objectExists(filename)).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("object meta", async () => {
|
||||||
|
const result = await adapter.getObjectMeta(filename);
|
||||||
|
console.log("objectMeta:result", result);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.type).toBe("image/png");
|
||||||
|
expect(result.size).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("list objects", async () => {
|
||||||
|
const result = await adapter.listObjects();
|
||||||
|
console.log("listObjects:result", result);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { randomString } from "../../../src/core/utils";
|
||||||
|
import { StorageLocalAdapter } from "../../../src/media/storage/adapters/StorageLocalAdapter";
|
||||||
|
import { assetsPath, assetsTmpPath } from "../../helper";
|
||||||
|
|
||||||
|
describe("StorageLocalAdapter", () => {
|
||||||
|
const adapter = new StorageLocalAdapter({
|
||||||
|
path: assetsTmpPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
const file = Bun.file(`${assetsPath}/image.png`);
|
||||||
|
const _filename = randomString(10);
|
||||||
|
const filename = `${_filename}.png`;
|
||||||
|
|
||||||
|
let objects = 0;
|
||||||
|
|
||||||
|
test("puts an object", async () => {
|
||||||
|
objects = (await adapter.listObjects()).length;
|
||||||
|
expect(await adapter.putObject(filename, file as unknown as File)).toBeString();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("lists objects", async () => {
|
||||||
|
expect((await adapter.listObjects()).length).toBe(objects + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("file exists", async () => {
|
||||||
|
expect(await adapter.objectExists(filename)).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("gets an object", async () => {
|
||||||
|
const res = await adapter.getObject(filename, new Headers());
|
||||||
|
expect(res.ok).toBeTrue();
|
||||||
|
// @todo: check the content
|
||||||
|
});
|
||||||
|
|
||||||
|
test("gets object meta", async () => {
|
||||||
|
expect(await adapter.getObjectMeta(filename)).toEqual({
|
||||||
|
type: file.type, // image/png
|
||||||
|
size: file.size,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("deletes an object", async () => {
|
||||||
|
expect(await adapter.deleteObject(filename)).toBeUndefined();
|
||||||
|
expect(await adapter.objectExists(filename)).toBeFalse();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
|
import { randomString } from "../../../src/core/utils";
|
||||||
|
import { StorageS3Adapter } from "../../../src/media";
|
||||||
|
|
||||||
|
import { config } from "dotenv";
|
||||||
|
//import { enableFetchLogging } from "../../helper";
|
||||||
|
const dotenvOutput = config({ path: `${import.meta.dir}/../../../.env` });
|
||||||
|
const { R2_ACCESS_KEY, R2_SECRET_ACCESS_KEY, R2_URL, AWS_ACCESS_KEY, AWS_SECRET_KEY, AWS_S3_URL } =
|
||||||
|
dotenvOutput.parsed!;
|
||||||
|
|
||||||
|
// @todo: mock r2/s3 responses for faster tests
|
||||||
|
const ALL_TESTS = !!process.env.ALL_TESTS;
|
||||||
|
console.log("ALL_TESTS?", ALL_TESTS);
|
||||||
|
|
||||||
|
/*
|
||||||
|
// @todo: preparation to mock s3 calls + replace fast-xml-parser
|
||||||
|
let cleanup: () => void;
|
||||||
|
beforeAll(async () => {
|
||||||
|
cleanup = await enableFetchLogging();
|
||||||
|
});
|
||||||
|
afterAll(() => {
|
||||||
|
cleanup();
|
||||||
|
}); */
|
||||||
|
|
||||||
|
describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => {
|
||||||
|
if (ALL_TESTS) return;
|
||||||
|
|
||||||
|
const versions = [
|
||||||
|
[
|
||||||
|
"r2",
|
||||||
|
new StorageS3Adapter({
|
||||||
|
access_key: R2_ACCESS_KEY as string,
|
||||||
|
secret_access_key: R2_SECRET_ACCESS_KEY as string,
|
||||||
|
url: R2_URL as string,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"s3",
|
||||||
|
new StorageS3Adapter({
|
||||||
|
access_key: AWS_ACCESS_KEY as string,
|
||||||
|
secret_access_key: AWS_SECRET_KEY as string,
|
||||||
|
url: AWS_S3_URL as string,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const _conf = {
|
||||||
|
adapters: ["r2", "s3"],
|
||||||
|
tests: [
|
||||||
|
"listObjects",
|
||||||
|
"putObject",
|
||||||
|
"objectExists",
|
||||||
|
"getObject",
|
||||||
|
"deleteObject",
|
||||||
|
"getObjectMeta",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const file = Bun.file(`${import.meta.dir}/icon.png`);
|
||||||
|
const filename = `${randomString(10)}.png`;
|
||||||
|
|
||||||
|
// single (dev)
|
||||||
|
//_conf = { adapters: [/*"r2",*/ "s3"], tests: [/*"putObject",*/ "listObjects"] };
|
||||||
|
|
||||||
|
function disabled(test: (typeof _conf.tests)[number]) {
|
||||||
|
return !_conf.tests.includes(test);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @todo: add mocked fetch for faster tests
|
||||||
|
describe.each(versions)("StorageS3Adapter for %s", async (name, adapter) => {
|
||||||
|
if (!_conf.adapters.includes(name) || ALL_TESTS) {
|
||||||
|
console.log("Skipping", name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let objects = 0;
|
||||||
|
|
||||||
|
test.skipIf(disabled("putObject"))("puts an object", async () => {
|
||||||
|
objects = (await adapter.listObjects()).length;
|
||||||
|
expect(await adapter.putObject(filename, file as any)).toBeString();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skipIf(disabled("listObjects"))("lists objects", async () => {
|
||||||
|
expect((await adapter.listObjects()).length).toBe(objects + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skipIf(disabled("objectExists"))("file exists", async () => {
|
||||||
|
expect(await adapter.objectExists(filename)).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skipIf(disabled("getObject"))("gets an object", async () => {
|
||||||
|
const res = await adapter.getObject(filename, new Headers());
|
||||||
|
expect(res.ok).toBeTrue();
|
||||||
|
// @todo: check the content
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skipIf(disabled("getObjectMeta"))("gets object meta", async () => {
|
||||||
|
expect(await adapter.getObjectMeta(filename)).toEqual({
|
||||||
|
type: file.type, // image/png
|
||||||
|
size: file.size,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skipIf(disabled("deleteObject"))("deletes an object", async () => {
|
||||||
|
expect(await adapter.deleteObject(filename)).toBeUndefined();
|
||||||
|
expect(await adapter.objectExists(filename)).toBeFalse();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,9 +5,7 @@ import { getRandomizedFilename } from "../../src/media/utils";
|
|||||||
|
|
||||||
describe("media/mime-types", () => {
|
describe("media/mime-types", () => {
|
||||||
test("tiny resolves", () => {
|
test("tiny resolves", () => {
|
||||||
const tests = [
|
const tests = [[".mp4", "video/mp4", ".jpg", "image/jpeg", ".zip", "application/zip"]];
|
||||||
[".mp4", "video/mp4", ".jpg", "image/jpeg", ".zip", "application/zip"],
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
for (const [ext, mime] of tests) {
|
for (const [ext, mime] of tests) {
|
||||||
expect(tiny.guess(ext)).toBe(mime);
|
expect(tiny.guess(ext)).toBe(mime);
|
||||||
@@ -71,9 +69,7 @@ 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;
|
|
||||||
|
|
||||||
for (const [mime, ext] of tests) {
|
for (const [mime, ext] of tests) {
|
||||||
expect(tiny.extension(mime), `extension(): ${mime} should be ${ext}`).toBe(ext);
|
expect(tiny.extension(mime), `extension(): ${mime} should be ${ext}`).toBe(ext);
|
||||||
@@ -90,10 +86,7 @@ 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;
|
|
||||||
|
|
||||||
for (const [filename, ext] of tests) {
|
for (const [filename, ext] of tests) {
|
||||||
expect(
|
expect(
|
||||||
@@ -101,42 +94,5 @@ describe("media/mime-types", () => {
|
|||||||
`getRandomizedFilename(): ${filename} should end with ${ext}`,
|
`getRandomizedFilename(): ${filename} should end with ${ext}`,
|
||||||
).toBe(ext);
|
).toBe(ext);
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure it keeps the extension, even if the file has a different type
|
|
||||||
const file = new File([""], "image.jpg", { type: "text/plain" });
|
|
||||||
const [, ext] = getRandomizedFilename(file).split(".");
|
|
||||||
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;
|
||||||
@@ -77,7 +69,7 @@ describe("AppAuth", () => {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
email: "some@body.com",
|
email: "some@body.com",
|
||||||
password: "12345678",
|
password: "123456",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
enableConsoleLog();
|
enableConsoleLog();
|
||||||
@@ -89,68 +81,9 @@ describe("AppAuth", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("creates user on register (bcrypt)", async () => {
|
|
||||||
const auth = new AppAuth(
|
|
||||||
{
|
|
||||||
enabled: true,
|
|
||||||
strategies: {
|
|
||||||
password: {
|
|
||||||
type: "password",
|
|
||||||
config: {
|
|
||||||
hashing: "bcrypt",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// @ts-ignore
|
|
||||||
jwt: {
|
|
||||||
secret: "123456",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ctx,
|
|
||||||
);
|
|
||||||
|
|
||||||
await auth.build();
|
|
||||||
await ctx.em.schema().sync({ force: true });
|
|
||||||
|
|
||||||
// expect no users, but the query to pass
|
|
||||||
const res = await ctx.em.repository("users").findMany();
|
|
||||||
expect(res.data.length).toBe(0);
|
|
||||||
|
|
||||||
const app = new AuthController(auth).getController();
|
|
||||||
|
|
||||||
{
|
|
||||||
disableConsoleLog();
|
|
||||||
const res = await app.request("/password/register", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: "some@body.com",
|
|
||||||
password: "12345678",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
enableConsoleLog();
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
|
|
||||||
const { data: users } = await ctx.em.repository("users").findMany();
|
|
||||||
expect(users.length).toBe(1);
|
|
||||||
expect(users[0]?.email).toBe("some@body.com");
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// check user in database
|
|
||||||
const rawUser = await ctx.connection.kysely
|
|
||||||
.selectFrom("users")
|
|
||||||
.selectAll()
|
|
||||||
.executeTakeFirstOrThrow();
|
|
||||||
expect(rawUser.strategy_value).toStartWith("$");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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: {
|
||||||
@@ -161,7 +94,6 @@ describe("AppAuth", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await app.build();
|
await app.build();
|
||||||
app.registerAdminController();
|
|
||||||
const spy = spyOn(app.module.auth.authenticator, "requestCookieRefresh");
|
const spy = spyOn(app.module.auth.authenticator, "requestCookieRefresh");
|
||||||
|
|
||||||
// register custom route
|
// register custom route
|
||||||
@@ -171,16 +103,12 @@ describe("AppAuth", () => {
|
|||||||
await app.server.request("/api/system/ping");
|
await app.server.request("/api/system/ping");
|
||||||
await app.server.request("/test");
|
await app.server.request("/test");
|
||||||
|
|
||||||
expect(spy.mock.calls.length).toBe(0);
|
|
||||||
|
|
||||||
// admin route
|
|
||||||
await app.server.request("/");
|
|
||||||
expect(spy.mock.calls.length).toBe(1);
|
expect(spy.mock.calls.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
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 +132,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,51 +1,13 @@
|
|||||||
import { beforeEach, describe, expect, test } from "bun:test";
|
import { 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 } from "../../src/modules";
|
||||||
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
import { moduleTestSuite } from "./module-test-suite";
|
||||||
import * as proto from "data/prototype";
|
|
||||||
|
|
||||||
describe("AppData", () => {
|
describe("AppData", () => {
|
||||||
moduleTestSuite(AppData);
|
moduleTestSuite(AppData);
|
||||||
|
|
||||||
let ctx: ModuleBuildContext;
|
|
||||||
beforeEach(() => {
|
|
||||||
ctx = makeCtx();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("field config construction", () => {
|
test("field config construction", () => {
|
||||||
expect(parse(fieldsSchema, { type: "text" })).toBeDefined();
|
expect(parse(fieldsSchema, { type: "text" })).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should prevent multi-deletion of entities in single request", async () => {
|
|
||||||
const schema = proto.em({
|
|
||||||
one: proto.entity("one", {
|
|
||||||
text: proto.text(),
|
|
||||||
}),
|
|
||||||
two: proto.entity("two", {
|
|
||||||
text: proto.text(),
|
|
||||||
}),
|
|
||||||
three: proto.entity("three", {
|
|
||||||
text: proto.text(),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
const check = () => {
|
|
||||||
const expected = ["one", "two", "three"];
|
|
||||||
const fromConfig = Object.keys(data.config.entities ?? {});
|
|
||||||
const fromEm = data.em.entities.map((e) => e.name);
|
|
||||||
expect(fromConfig).toEqual(expected);
|
|
||||||
expect(fromEm).toEqual(expected);
|
|
||||||
};
|
|
||||||
|
|
||||||
// auth must be enabled, otherwise default config is returned
|
|
||||||
const data = new AppData(schema.toJSON(), ctx);
|
|
||||||
await data.build();
|
|
||||||
check();
|
|
||||||
|
|
||||||
expect(data.schema().remove("entities")).rejects.toThrow(/more than one entity/);
|
|
||||||
check();
|
|
||||||
|
|
||||||
await data.setContext(makeCtx()).build();
|
|
||||||
check();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 "../../src/media/storage/adapters/StorageLocalAdapter";
|
||||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
import { AppMedia } from "../../src/modules";
|
||||||
import { AppMedia } from "../../src/media/AppMedia";
|
|
||||||
import { moduleTestSuite } from "./module-test-suite";
|
import { moduleTestSuite } from "./module-test-suite";
|
||||||
|
|
||||||
describe("AppMedia", () => {
|
describe("AppMedia", () => {
|
||||||
test.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,23 +1,23 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { s, stripMark } from "core/utils/schema";
|
import { type TSchema, Type, stripMark } from "../../src/core/utils";
|
||||||
import { em, entity, index, text } from "data/prototype";
|
import { EntityManager, em, entity, index, text } from "../../src/data";
|
||||||
import { EntityManager } from "data/entities/EntityManager";
|
|
||||||
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
||||||
import { Module } from "../../src/modules/Module";
|
import { Module } from "../../src/modules/Module";
|
||||||
import { ModuleHelper } from "modules/ModuleHelper";
|
|
||||||
|
|
||||||
function createModule<Schema extends 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;
|
||||||
}
|
}
|
||||||
override toJSON() {
|
toJSON() {
|
||||||
return this.config;
|
return this.config;
|
||||||
}
|
}
|
||||||
override useForceParse() {
|
useForceParse() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
return TestModule;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Module", async () => {
|
describe("Module", async () => {
|
||||||
@@ -25,7 +25,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 +41,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 +62,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 +142,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 +158,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,10 @@
|
|||||||
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 { Type, disableConsoleLog, enableConsoleLog, stripMark } from "../../src/core/utils";
|
||||||
|
import { entity, text } from "../../src/data";
|
||||||
import { Module } from "modules/Module";
|
import { Module } from "../../src/modules/Module";
|
||||||
import { getDefaultConfig } from "modules/ModuleManager";
|
import { ModuleManager, getDefaultConfig } from "../../src/modules/ModuleManager";
|
||||||
import { type ConfigTable, DbModuleManager as ModuleManager } from "modules/db/DbModuleManager";
|
import { CURRENT_VERSION, TABLE_NAME } from "../../src/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 { Connection } from "data/connection/Connection";
|
|
||||||
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 +89,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 +126,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 +254,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;
|
||||||
}
|
}
|
||||||
@@ -392,128 +380,4 @@ describe("ModuleManager", async () => {
|
|||||||
expect(() => f.default()).toThrow();
|
expect(() => f.default()).toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function getRawConfig(c: Connection) {
|
|
||||||
return (await c.kysely
|
|
||||||
.selectFrom(TABLE_NAME)
|
|
||||||
.selectAll()
|
|
||||||
.where("type", "=", "config")
|
|
||||||
.orderBy("version", "desc")
|
|
||||||
.executeTakeFirstOrThrow()) as unknown as ConfigTable;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getDiffs(c: Connection, opts?: { dir?: "asc" | "desc"; limit?: number }) {
|
|
||||||
return await c.kysely
|
|
||||||
.selectFrom(TABLE_NAME)
|
|
||||||
.selectAll()
|
|
||||||
.where("type", "=", "diff")
|
|
||||||
.orderBy("version", opts?.dir ?? "desc")
|
|
||||||
.$if(!!opts?.limit, (b) => b.limit(opts!.limit!))
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("diffs", () => {
|
|
||||||
test("never empty", async () => {
|
|
||||||
const { dummyConnection: c } = getDummyConnection();
|
|
||||||
const mm = new ModuleManager(c);
|
|
||||||
await mm.build();
|
|
||||||
await mm.save();
|
|
||||||
expect(await getDiffs(c)).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("has timestamps", async () => {
|
|
||||||
const { dummyConnection: c } = getDummyConnection();
|
|
||||||
const mm = new ModuleManager(c);
|
|
||||||
await mm.build();
|
|
||||||
|
|
||||||
await mm.get("data").schema().patch("basepath", "/api/data2");
|
|
||||||
await mm.save();
|
|
||||||
|
|
||||||
const config = await getRawConfig(c);
|
|
||||||
const diffs = await getDiffs(c);
|
|
||||||
|
|
||||||
expect(config.json.data.basepath).toBe("/api/data2");
|
|
||||||
expect(diffs).toHaveLength(1);
|
|
||||||
expect(diffs[0]!.created_at).toBeDefined();
|
|
||||||
expect(diffs[0]!.updated_at).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("validate & revert", () => {
|
|
||||||
const schema = s.object({
|
|
||||||
value: s.array(s.number()),
|
|
||||||
});
|
|
||||||
type SampleSchema = s.Static<typeof schema>;
|
|
||||||
class Sample extends Module<SampleSchema> {
|
|
||||||
getSchema() {
|
|
||||||
return schema;
|
|
||||||
}
|
|
||||||
override async build() {
|
|
||||||
this.setBuilt();
|
|
||||||
}
|
|
||||||
override async onBeforeUpdate(from: SampleSchema, to: SampleSchema) {
|
|
||||||
if (to.value.length > 3) {
|
|
||||||
throw new Error("too many values");
|
|
||||||
}
|
|
||||||
if (to.value.includes(7)) {
|
|
||||||
throw new Error("contains 7");
|
|
||||||
}
|
|
||||||
|
|
||||||
return to;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class TestModuleManager extends ModuleManager {
|
|
||||||
constructor(...args: ConstructorParameters<typeof ModuleManager>) {
|
|
||||||
super(...args);
|
|
||||||
this.modules["module1"] = new Sample({}, this.ctx());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
test("respects module onBeforeUpdate", async () => {
|
|
||||||
const { dummyConnection: c } = getDummyConnection();
|
|
||||||
const mm = new TestModuleManager(c);
|
|
||||||
await mm.build();
|
|
||||||
|
|
||||||
const m = mm.get("module1" as any) as Sample;
|
|
||||||
|
|
||||||
{
|
|
||||||
expect(async () => {
|
|
||||||
await m.schema().set({ value: [1, 2, 3, 4, 5] });
|
|
||||||
return mm.save();
|
|
||||||
}).toThrow(/too many values/);
|
|
||||||
|
|
||||||
expect(m.config.value).toHaveLength(0);
|
|
||||||
expect((mm.configs() as any).module1.value).toHaveLength(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
expect(async () => {
|
|
||||||
await mm.mutateConfigSafe("module1" as any).set({ value: [1, 2, 3, 4, 5] });
|
|
||||||
return mm.save();
|
|
||||||
}).toThrow(/too many values/);
|
|
||||||
|
|
||||||
expect(m.config.value).toHaveLength(0);
|
|
||||||
expect((mm.configs() as any).module1.value).toHaveLength(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
expect(async () => {
|
|
||||||
await m.schema().set({ value: [1, 7, 5] });
|
|
||||||
return mm.save();
|
|
||||||
}).toThrow(/contains 7/);
|
|
||||||
|
|
||||||
expect(m.config.value).toHaveLength(0);
|
|
||||||
expect((mm.configs() as any).module1.value).toHaveLength(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
expect(async () => {
|
|
||||||
await mm.mutateConfigSafe("module1" as any).set({ value: [1, 7, 5] });
|
|
||||||
return mm.save();
|
|
||||||
}).toThrow(/contains 7/);
|
|
||||||
|
|
||||||
expect(m.config.value).toHaveLength(0);
|
|
||||||
expect((mm.configs() as any).module1.value).toHaveLength(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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(), {}));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user