mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 08:06:00 +00:00
Compare commits
224 Commits
v0.9.0
...
feat/mm-diffs
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e25d54847 | |||
| 9d5bc61f33 | |||
| ea46dc8df0 | |||
| 88ec3432a0 | |||
| 43c8f14470 | |||
| c384bf4dd4 | |||
| db2a994a01 | |||
| 166ea4a71b | |||
| 0d3bb3b7d6 | |||
| cfbec5b6ea | |||
| 957355c862 | |||
| fc282b795e | |||
| bc65f5f311 | |||
| 5355f2593e | |||
| e939debba5 | |||
| b35ee36fb1 | |||
| 109c72e84f | |||
| 7e0b02a125 | |||
| d4076803c6 | |||
| a5959acb34 | |||
| a4ddf780ce | |||
| 2f684765de | |||
| 5143ee5726 | |||
| 22b54862e8 | |||
| fe1716ed01 | |||
| 0c31dcdb95 | |||
| 4cc0f8e172 | |||
| 45138c25f0 | |||
| 80034b9b0a | |||
| e6ee75c712 | |||
| 56287eb05e | |||
| 144f35ec66 | |||
| d1378c6c51 | |||
| 4e10b36d0d | |||
| 70737c04cd | |||
| d41fd5541f | |||
| e76e3531d7 | |||
| a9f3a582eb | |||
| 57ae2f333c | |||
| c161a26ec0 | |||
| 6e78a4c238 | |||
| 42edce904f | |||
| b2086c4da7 | |||
| 344d729320 | |||
| c86f4c12b7 | |||
| aaa97ed113 | |||
| 69c8aec6fb | |||
| f5ceffd80e | |||
| a9f367aa2c | |||
| b87696a0db | |||
| 3338804c34 | |||
| 6b3ac9e6e2 | |||
| 8e1ecfcfe3 | |||
| 6e2596fb77 | |||
| 2ada4e9f20 | |||
| cc038a0a9a | |||
| af6d1960b9 | |||
| 96ae8f9d63 | |||
| c09d1d53b3 | |||
| 2239333f08 | |||
| 0710b5bf1f | |||
| 03c4c41d2d | |||
| 85d8542a2e | |||
| f8d2a9090e | |||
| bbb7bfb7a1 | |||
| 28e277afe1 | |||
| 8517c9b90b | |||
| fe5ccd4206 | |||
| 8b4b63b3cd | |||
| 9c4aac8843 | |||
| d5bb6ffa61 | |||
| fc513bb413 | |||
| 4162b9878a | |||
| c75f8d0937 | |||
| 046c1d21b1 | |||
| 6c2e579596 | |||
| 88419548c7 | |||
| 12c955155d | |||
| 15af9f5261 | |||
| 39efb911e9 | |||
| 4729203d47 | |||
| 0db052acca | |||
| 6e08f45857 | |||
| 7bff84d601 | |||
| e66e05b2b0 | |||
| 5b318ce485 | |||
| 22de16fe17 | |||
| 5d26673bc6 | |||
| dfa4bc0e33 | |||
| e51b89a18a | |||
| 63988e0c5f | |||
| 297fd85a4f | |||
| a82fbe7400 | |||
| 74e1e9a03f | |||
| 05a81b5bdc | |||
| 9ff49103fb | |||
| a5b59c004e | |||
| a0edcf483b | |||
| 9a18e354cd | |||
| 58c7aba1a4 | |||
| f8aa242d2b | |||
| 3da9570abe | |||
| 6ee898e606 | |||
| abbd372ddf | |||
| 3e77982996 | |||
| 3fbea8ace7 | |||
| a41f943b43 | |||
| 15a9c549e7 | |||
| 50cadbaa8e | |||
| 7b128c9701 | |||
| 061181d59d | |||
| af6cb0c8f0 | |||
| 5a693c0370 | |||
| 262588decc | |||
| 17ab35e245 | |||
| db795ec050 | |||
| 773df544dd | |||
| 0ac7d1fd6e | |||
| 6694c63990 | |||
| b3f95f9552 | |||
| 372f94d22a | |||
| d6f94a2ce1 | |||
| 89a39a7dc6 | |||
| 88cf65f792 | |||
| 07723ce6ae | |||
| 9010401af6 | |||
| 4c11789ea8 | |||
| 2988e4c3bd | |||
| ad7926db4c | |||
| 53a3dcdee7 | |||
| 53467d6750 | |||
| a80a731498 | |||
| 7e1757b7f4 | |||
| 2c29e06fb8 | |||
| ca86fa58ac | |||
| de984fa101 | |||
| a12d4e13d0 | |||
| fa6c7acaf5 | |||
| 0b41aa5a2d | |||
| a5ec40c517 | |||
| 75e2b96344 | |||
| aaae8d9681 | |||
| e4608b7df7 | |||
| 44b3f72005 | |||
| 9134d121cd | |||
| 2f067451b4 | |||
| 3f26c45dd9 | |||
| 36e4224b33 | |||
| 434d56672c | |||
| b2fd907e8c | |||
| b29c04e8c9 | |||
| 11a28eba88 | |||
| 9e3c081e50 | |||
| 40c9ef9d90 | |||
| 7facef47da | |||
| e2bf6a0724 | |||
| f6a511d998 | |||
| 9407f3d212 | |||
| 0424c08a9e | |||
| 9d122da4b9 | |||
| 0f56d554b0 | |||
| 06e92d268b | |||
| b44b87923b | |||
| 6e6c65b375 | |||
| c9c00adf6c | |||
| ee15150c78 | |||
| f8f5ef9c98 | |||
| ec015b7849 | |||
| 67e0374c04 | |||
| 9380091ba9 | |||
| aa66a81b27 | |||
| 0c15ec1434 | |||
| 7b8c7f1ae4 | |||
| f6996b1953 | |||
| ea2aa7c76c | |||
| 2531c2d8d4 | |||
| 0e81e14421 | |||
| b994c2dfb0 | |||
| 4085d569a8 | |||
| 622a7b2b9a | |||
| 5697b7891a | |||
| 6a6a7a9f26 | |||
| b763826754 | |||
| b3a0ecbe6d | |||
| 181443b320 | |||
| 6015acb914 | |||
| a55d93b5ff | |||
| 1ed94716bd | |||
| 178838e06c | |||
| c4aaa5a90b | |||
| b1b2e97a5a | |||
| e9f2da82d7 | |||
| 9cba9ada84 | |||
| c8dcf964bc | |||
| 1ea22248b4 | |||
| 4f01004262 | |||
| 051c508254 | |||
| a72b11e9fb | |||
| 96781f5d3d | |||
| de89e26ffb | |||
| 166a7d4d0a | |||
| 640f015812 | |||
| d90f14c707 | |||
| 0922d90aff | |||
| 81ca7c0e28 | |||
| 4f87f6857f | |||
| 0dd29c3b07 | |||
| eb451a8e1a | |||
| 37df32b51b | |||
| 722d0bd29e | |||
| 03d3ab8b80 | |||
| dc2d39fa14 | |||
| 368c60d7c7 | |||
| 8df30f4dae | |||
| 2d7257d2bc | |||
| 7e2de1bd0c | |||
| b786fbcd13 | |||
| eabae5d3df | |||
| a4cb012ce8 | |||
| 3704cad53a | |||
| 25a3cb9655 | |||
| a5c422d45d | |||
| 8550aef606 | |||
| e82b72275b |
@@ -12,15 +12,28 @@ 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: latest
|
bun-version: "1.2.14"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
working-directory: ./app
|
working-directory: ./app
|
||||||
run: bun install
|
run: bun install
|
||||||
|
|
||||||
- name: Run tests
|
- name: Build
|
||||||
working-directory: ./app
|
working-directory: ./app
|
||||||
run: bun run test
|
run: bun run build:ci
|
||||||
|
|
||||||
|
- name: Run Bun tests
|
||||||
|
working-directory: ./app
|
||||||
|
run: bun run test:bun
|
||||||
|
|
||||||
|
- name: Run Node tests
|
||||||
|
working-directory: ./app
|
||||||
|
run: npm run test:node
|
||||||
@@ -30,3 +30,5 @@ packages/media/.env
|
|||||||
.vscode
|
.vscode
|
||||||
.git_old
|
.git_old
|
||||||
docker/tmp
|
docker/tmp
|
||||||
|
.debug
|
||||||
|
.history
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
[](https://npmjs.org/package/bknd)
|
[](https://npmjs.org/package/bknd)
|
||||||
[](https://www.npmjs.com/package/bknd)
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -10,22 +9,30 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
bknd simplifies app development by providing a fully functional backend for database management, authentication, media and workflows. Being lightweight and built on Web Standards, it can be deployed nearly anywhere, including running inside your framework of choice. No more deploying multiple separate services!
|
bknd simplifies app development by providing a fully functional backend for database management, authentication, media and workflows. Being lightweight and built on Web Standards, it can be deployed nearly anywhere, including running inside your framework of choice. No more deploying multiple separate services!
|
||||||
|
* **Runtimes**: Node.js 22+, Bun 1.0+, Deno, Browser, Cloudflare Workers/Pages, Vercel, Netlify, AWS Lambda, etc.
|
||||||
|
* **Databases**:
|
||||||
|
* SQLite: LibSQL, Node SQLite, Bun SQLite, Cloudflare D1, Cloudflare Durable Objects SQLite, SQLocal
|
||||||
|
* Postgres: Vanilla Postgres, Supabase, Neon, Xata
|
||||||
|
* **Frameworks**: React, Next.js, React Router, Astro, Vite, Waku
|
||||||
|
* **Storage**: AWS S3, S3-compatible (Tigris, R2, Minio, etc.), Cloudflare R2 (binding), Cloudinary, Filesystem
|
||||||
|
|
||||||
**For documentation and examples, please visit https://docs.bknd.io.**
|
**For documentation and examples, please visit https://docs.bknd.io.**
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
> This project requires Node.js 22 or higher (because of `node:sqlite`).
|
||||||
|
>
|
||||||
> Please keep in mind that **bknd** is still under active development
|
> Please keep in mind that **bknd** is still under active development
|
||||||
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
|
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
|
||||||
|
|
||||||
## 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 212 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 300 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.
|
||||||
@@ -42,7 +49,7 @@ Creating digital products always requires developing both the backend (the logic
|
|||||||
* 🏃♂️ 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)
|
||||||
* using a React framework (Astro, Remix, Next.js)
|
* using a React framework (Next.js, React Router, Astro)
|
||||||
* 📦 Official API and React SDK with type-safety
|
* 📦 Official API and React SDK with type-safety
|
||||||
* ⚛️ React elements for auto-configured authentication and media components
|
* ⚛️ React elements for auto-configured authentication and media components
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# ===== 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=
|
||||||
|
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
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
playwright-report
|
||||||
|
test-results
|
||||||
|
bknd.config.*
|
||||||
|
__test__/helper.d.ts
|
||||||
+134
-4
@@ -1,15 +1,145 @@
|
|||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterEach, describe, test, expect } from "bun:test";
|
||||||
import { App } from "../src";
|
import { App, createApp } from "core/test/utils";
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
|
import { Hono } from "hono";
|
||||||
|
import * as proto from "../src/data/prototype";
|
||||||
|
import { pick } from "lodash-es";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
afterAll(afterAllCleanup);
|
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.data?.em.ping()).toBeTrue();
|
expect(await app.em.ping()).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("plugins", async () => {
|
||||||
|
const called: string[] = [];
|
||||||
|
const app = createApp({
|
||||||
|
initialConfig: {
|
||||||
|
auth: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
plugins: [
|
||||||
|
(app) => {
|
||||||
|
expect(app).toBeDefined();
|
||||||
|
expect(app).toBeInstanceOf(App);
|
||||||
|
return {
|
||||||
|
name: "test",
|
||||||
|
schema: () => {
|
||||||
|
called.push("schema");
|
||||||
|
return proto.em(
|
||||||
|
{
|
||||||
|
posts: proto.entity("posts", {
|
||||||
|
title: proto.text(),
|
||||||
|
}),
|
||||||
|
comments: proto.entity("comments", {
|
||||||
|
content: proto.text(),
|
||||||
|
}),
|
||||||
|
users: proto.entity("users", {
|
||||||
|
email_verified: proto.boolean(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
(fn, s) => {
|
||||||
|
fn.relation(s.comments).manyToOne(s.posts);
|
||||||
|
fn.index(s.posts).on(["title"]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onBoot: async () => {
|
||||||
|
called.push("onBoot");
|
||||||
|
},
|
||||||
|
beforeBuild: async () => {
|
||||||
|
called.push("beforeBuild");
|
||||||
|
},
|
||||||
|
onBuilt: async () => {
|
||||||
|
called.push("onBuilt");
|
||||||
|
},
|
||||||
|
onServerInit: async (server) => {
|
||||||
|
called.push("onServerInit");
|
||||||
|
expect(server).toBeDefined();
|
||||||
|
expect(server).toBeInstanceOf(Hono);
|
||||||
|
},
|
||||||
|
onFirstBoot: async () => {
|
||||||
|
called.push("onFirstBoot");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.build();
|
||||||
|
|
||||||
|
expect(app.em.entities.map((e) => e.name)).toEqual(["users", "posts", "comments"]);
|
||||||
|
expect(app.em.indices.map((i) => i.name)).toEqual([
|
||||||
|
"idx_unique_users_email",
|
||||||
|
"idx_users_strategy",
|
||||||
|
"idx_users_strategy_value",
|
||||||
|
"idx_posts_title",
|
||||||
|
]);
|
||||||
|
expect(
|
||||||
|
app.em.relations.all.map((r) => pick(r.toJSON(), ["type", "source", "target"])),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
type: "n:1",
|
||||||
|
source: "comments",
|
||||||
|
target: "posts",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(called).toEqual([
|
||||||
|
"onBoot",
|
||||||
|
"onServerInit",
|
||||||
|
"beforeBuild",
|
||||||
|
"onServerInit",
|
||||||
|
"schema",
|
||||||
|
"onFirstBoot",
|
||||||
|
"onBuilt",
|
||||||
|
]);
|
||||||
|
expect(app.plugins.size).toBe(1);
|
||||||
|
expect(Array.from(app.plugins.keys())).toEqual(["test"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.only("drivers", async () => {
|
||||||
|
const called: string[] = [];
|
||||||
|
const app = new App(dummyConnection, undefined, {
|
||||||
|
drivers: {
|
||||||
|
email: {
|
||||||
|
send: async (to, subject, body) => {
|
||||||
|
called.push("email.send");
|
||||||
|
return {
|
||||||
|
id: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cache: {
|
||||||
|
get: async (key) => {
|
||||||
|
called.push("cache.get");
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
set: async (key, value, ttl) => {
|
||||||
|
called.push("cache.set");
|
||||||
|
},
|
||||||
|
del: async (key) => {
|
||||||
|
called.push("cache.del");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await app.build();
|
||||||
|
|
||||||
|
expect(app.drivers.cache).toBeDefined();
|
||||||
|
expect(app.drivers.email).toBeDefined();
|
||||||
|
await app.drivers.email.send("", "", "");
|
||||||
|
await app.drivers.cache.get("");
|
||||||
|
await app.drivers.cache.set("", "", 0);
|
||||||
|
await app.drivers.cache.del("");
|
||||||
|
|
||||||
|
expect(called).toEqual(["email.send", "cache.get", "cache.set", "cache.del"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,77 @@
|
|||||||
|
import { expect, describe, it, beforeAll, afterAll } from "bun:test";
|
||||||
|
import * as adapter from "adapter";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||||
|
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||||
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
|
import { omitKeys } from "core/utils";
|
||||||
|
|
||||||
|
beforeAll(disableConsoleLog);
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
|
describe("adapter", () => {
|
||||||
|
it("makes config", () => {
|
||||||
|
expect(omitKeys(adapter.makeConfig({}), ["connection"])).toEqual({});
|
||||||
|
expect(omitKeys(adapter.makeConfig({}, { env: { TEST: "test" } }), ["connection"])).toEqual(
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
// merges everything returned from `app` with the config
|
||||||
|
expect(
|
||||||
|
omitKeys(
|
||||||
|
adapter.makeConfig(
|
||||||
|
{ app: (a) => ({ initialConfig: { server: { cors: { origin: a.env.TEST } } } }) },
|
||||||
|
{ env: { TEST: "test" } },
|
||||||
|
),
|
||||||
|
["connection"],
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
initialConfig: { server: { cors: { origin: "test" } } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* it.only("...", async () => {
|
||||||
|
const app = await adapter.createAdapterApp();
|
||||||
|
}); */
|
||||||
|
|
||||||
|
it("reuses apps correctly", async () => {
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
|
||||||
|
const first = await adapter.createAdapterApp(
|
||||||
|
{
|
||||||
|
initialConfig: { server: { cors: { origin: "random" } } },
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ id },
|
||||||
|
);
|
||||||
|
const second = await adapter.createAdapterApp();
|
||||||
|
const third = await adapter.createAdapterApp(undefined, undefined, { id });
|
||||||
|
|
||||||
|
await first.build();
|
||||||
|
await second.build();
|
||||||
|
await third.build();
|
||||||
|
|
||||||
|
expect(first.toJSON().server.cors.origin).toEqual("random");
|
||||||
|
expect(first).toBe(third);
|
||||||
|
expect(first).not.toBe(second);
|
||||||
|
expect(second).not.toBe(third);
|
||||||
|
expect(second.toJSON().server.cors.origin).toEqual("*");
|
||||||
|
|
||||||
|
// recreate the first one
|
||||||
|
const first2 = await adapter.createAdapterApp(undefined, undefined, { id, force: true });
|
||||||
|
await first2.build();
|
||||||
|
expect(first2).not.toBe(first);
|
||||||
|
expect(first2).not.toBe(third);
|
||||||
|
expect(first2).not.toBe(second);
|
||||||
|
expect(first2.toJSON().server.cors.origin).toEqual("*");
|
||||||
|
});
|
||||||
|
|
||||||
|
adapterTestSuite(bunTestRunner, {
|
||||||
|
makeApp: adapter.createFrameworkApp,
|
||||||
|
label: "framework app",
|
||||||
|
});
|
||||||
|
|
||||||
|
adapterTestSuite(bunTestRunner, {
|
||||||
|
makeApp: adapter.createRuntimeApp,
|
||||||
|
label: "runtime app",
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||||
import { Guard } from "../../src/auth";
|
import { Guard } from "../../src/auth";
|
||||||
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 { disableConsoleLog, enableConsoleLog, schemaToEm } from "../helper";
|
import { schemaToEm } from "../helper";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
import { parse } from "bknd/core";
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
beforeAll(disableConsoleLog);
|
||||||
afterAll(enableConsoleLog);
|
afterAll(enableConsoleLog);
|
||||||
@@ -64,6 +65,15 @@ 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 () => {
|
||||||
@@ -143,7 +153,7 @@ describe("DataApi", () => {
|
|||||||
const oneBy = api.readOneBy("posts", { where: { title: "baz" }, select: ["title"] });
|
const oneBy = api.readOneBy("posts", { where: { title: "baz" }, select: ["title"] });
|
||||||
const oneByRes = await oneBy;
|
const oneByRes = await oneBy;
|
||||||
expect(oneByRes.data).toEqual({ title: "baz" } as any);
|
expect(oneByRes.data).toEqual({ title: "baz" } as any);
|
||||||
expect(oneByRes.body.meta.count).toEqual(1);
|
expect(oneByRes.body.meta.items).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("exists/count", async () => {
|
it("exists/count", async () => {
|
||||||
|
|||||||
@@ -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 "../../src/core/utils";
|
import { getFileFromContext, isFile, isReadableStream } from "core/utils";
|
||||||
import { MediaApi } from "../../src/media/api/MediaApi";
|
import { MediaApi } from "media/api/MediaApi";
|
||||||
import { assetsPath, assetsTmpPath } from "../helper";
|
import { assetsPath, assetsTmpPath } from "../helper";
|
||||||
|
|
||||||
const mockedBackend = new Hono()
|
const mockedBackend = new Hono()
|
||||||
@@ -32,17 +32,35 @@ describe("MediaApi", () => {
|
|||||||
host,
|
host,
|
||||||
basepath,
|
basepath,
|
||||||
});
|
});
|
||||||
expect(api.getFileUploadUrl({ path: "path" })).toBe(`${host}${basepath}/upload/path`);
|
expect(api.getFileUploadUrl({ path: "path" } as any)).toBe(`${host}${basepath}/upload/path`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should have correct upload headers", () => {
|
it("should have correct upload headers", () => {
|
||||||
// @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}`;
|
||||||
@@ -103,8 +121,12 @@ describe("MediaApi", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should upload file in various ways", async () => {
|
it("should upload file in various ways", async () => {
|
||||||
// @ts-ignore tests
|
const api = new MediaApi(
|
||||||
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,6 +1,6 @@
|
|||||||
import { describe, expect, mock, test } from "bun:test";
|
import { describe, expect, mock, test } from "bun:test";
|
||||||
import type { ModuleBuildContext } from "../../src";
|
import type { ModuleBuildContext } from "../../src";
|
||||||
import { type App, createApp } from "../../src/App";
|
import { App, createApp } from "core/test/utils";
|
||||||
import * as proto from "../../src/data/prototype";
|
import * as proto from "../../src/data/prototype";
|
||||||
|
|
||||||
describe("App", () => {
|
describe("App", () => {
|
||||||
@@ -20,6 +20,7 @@ describe("App", () => {
|
|||||||
"guard",
|
"guard",
|
||||||
"flags",
|
"flags",
|
||||||
"logger",
|
"logger",
|
||||||
|
"helper",
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -51,4 +52,87 @@ 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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
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: {
|
||||||
|
origin: "*",
|
||||||
|
allow_methods: ["GET", "POST", "PATCH", "PUT", "DELETE"],
|
||||||
|
allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const server = new AppServer({
|
||||||
|
cors: {
|
||||||
|
origin: "https",
|
||||||
|
allow_methods: ["GET", "POST"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(server).toBeDefined();
|
||||||
|
expect(server.config).toEqual({
|
||||||
|
cors: {
|
||||||
|
origin: "https",
|
||||||
|
allow_methods: ["GET", "POST"],
|
||||||
|
allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { createApp, registries } from "../../src";
|
import { registries } from "../../src";
|
||||||
|
import { createApp } from "core/test/utils";
|
||||||
import * as proto from "../../src/data/prototype";
|
import * as proto from "../../src/data/prototype";
|
||||||
import { StorageLocalAdapter } from "../../src/media/storage/adapters/StorageLocalAdapter";
|
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||||
|
|
||||||
describe("repros", async () => {
|
describe("repros", async () => {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import { describe, expect, test } from "bun:test";
|
|||||||
import { Authenticator, type User, type UserPool } from "../../src/auth";
|
import { Authenticator, type User, type UserPool } from "../../src/auth";
|
||||||
import { cookieConfig } from "../../src/auth/authenticate/Authenticator";
|
import { cookieConfig } from "../../src/auth/authenticate/Authenticator";
|
||||||
import { PasswordStrategy } from "../../src/auth/authenticate/strategies/PasswordStrategy";
|
import { PasswordStrategy } from "../../src/auth/authenticate/strategies/PasswordStrategy";
|
||||||
import * as hash from "../../src/auth/utils/hash";
|
import { parse } from "bknd/core";
|
||||||
import { Default, parse } from "../../src/core/utils";
|
|
||||||
|
|
||||||
/*class MemoryUserPool implements UserPool {
|
/*class MemoryUserPool implements UserPool {
|
||||||
constructor(private users: User[] = []) {}
|
constructor(private users: User[] = []) {}
|
||||||
@@ -23,7 +22,7 @@ import { Default, parse } from "../../src/core/utils";
|
|||||||
describe("Authenticator", async () => {
|
describe("Authenticator", async () => {
|
||||||
test("cookie options", async () => {
|
test("cookie options", async () => {
|
||||||
console.log("parsed", parse(cookieConfig, undefined));
|
console.log("parsed", parse(cookieConfig, undefined));
|
||||||
console.log(Default(cookieConfig, {}));
|
console.log(cookieConfig.template({}));
|
||||||
});
|
});
|
||||||
/*const userpool = new MemoryUserPool([
|
/*const userpool = new MemoryUserPool([
|
||||||
{ id: 1, email: "d", username: "test", password: await hash.sha256("test") },
|
{ id: 1, email: "d", username: "test", password: await hash.sha256("test") },
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ 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 () => {
|
||||||
const strategy = new OAuthStrategy({
|
return;
|
||||||
|
/*const strategy = new OAuthStrategy({
|
||||||
type: "oidc",
|
type: "oidc",
|
||||||
client: {
|
client: {
|
||||||
client_id: process.env.OAUTH_CLIENT_ID,
|
client_id: process.env.OAUTH_CLIENT_ID!,
|
||||||
client_secret: process.env.OAUTH_CLIENT_SECRET,
|
client_secret: process.env.OAUTH_CLIENT_SECRET!,
|
||||||
},
|
},
|
||||||
name: "google",
|
name: "google",
|
||||||
});
|
});
|
||||||
@@ -19,13 +21,9 @@ describe("OAuthStrategy", async () => {
|
|||||||
const config = await strategy.getConfig();
|
const config = await strategy.getConfig();
|
||||||
console.log("config", JSON.stringify(config, null, 2));
|
console.log("config", JSON.stringify(config, null, 2));
|
||||||
|
|
||||||
const request = await strategy.request({
|
|
||||||
redirect_uri,
|
|
||||||
state,
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
||||||
@@ -39,8 +37,13 @@ describe("OAuthStrategy", async () => {
|
|||||||
return new Response("Bun!");
|
return new Response("Bun!");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const request = await strategy.request({
|
||||||
|
redirect_uri,
|
||||||
|
state,
|
||||||
|
});
|
||||||
console.log("request", request);
|
console.log("request", request);
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100000));
|
await new Promise((resolve) => setTimeout(resolve, 100000));
|
||||||
});
|
});*/
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ 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();
|
||||||
});
|
});
|
||||||
@@ -80,15 +83,11 @@ describe("EventManager", async () => {
|
|||||||
call();
|
call();
|
||||||
return Promise.all(p);
|
return Promise.all(p);
|
||||||
};
|
};
|
||||||
const emgr = new EventManager(
|
const emgr = new EventManager({ InformationalEvent });
|
||||||
{ 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();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -125,6 +124,9 @@ 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,7 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import type { TObject, TString } from "@sinclair/typebox";
|
import { Registry } from "core";
|
||||||
import { Registry } from "../../src/core/registry/Registry";
|
import { s } from "bknd/core";
|
||||||
import { type TSchema, Type } from "../../src/core/utils";
|
|
||||||
|
|
||||||
type Constructor<T> = new (...args: any[]) => T;
|
type Constructor<T> = new (...args: any[]) => T;
|
||||||
|
|
||||||
@@ -12,7 +11,7 @@ class What {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
getType() {
|
getType() {
|
||||||
return Type.Object({ type: Type.String() });
|
return s.object({ type: s.string() });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class What2 extends What {}
|
class What2 extends What {}
|
||||||
@@ -20,7 +19,7 @@ class NotAllowed {}
|
|||||||
|
|
||||||
type Test1 = {
|
type Test1 = {
|
||||||
cls: new (...args: any[]) => What;
|
cls: new (...args: any[]) => What;
|
||||||
schema: TObject<{ type: TString }>;
|
schema: s.ObjectSchema<{ type: s.StringSchema }>;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -29,7 +28,7 @@ describe("Registry", () => {
|
|||||||
const registry = new Registry<Test1>().set({
|
const registry = new Registry<Test1>().set({
|
||||||
first: {
|
first: {
|
||||||
cls: What,
|
cls: What,
|
||||||
schema: Type.Object({ type: Type.String(), what: Type.String() }),
|
schema: s.object({ type: s.string(), what: s.string() }),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
} satisfies Record<string, Test1>);
|
} satisfies Record<string, Test1>);
|
||||||
@@ -38,7 +37,7 @@ describe("Registry", () => {
|
|||||||
expect(item).toBeDefined();
|
expect(item).toBeDefined();
|
||||||
expect(item?.cls).toBe(What);
|
expect(item?.cls).toBe(What);
|
||||||
|
|
||||||
const second = Type.Object({ type: Type.String(), what: Type.String() });
|
const second = s.object({ type: s.string(), what: s.string() });
|
||||||
registry.add("second", {
|
registry.add("second", {
|
||||||
cls: What2,
|
cls: What2,
|
||||||
schema: second,
|
schema: second,
|
||||||
@@ -47,7 +46,7 @@ describe("Registry", () => {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(registry.get("second").schema).toEqual(second);
|
expect(registry.get("second").schema).toEqual(second);
|
||||||
|
|
||||||
const third = Type.Object({ type: Type.String({ default: "1" }), what22: Type.String() });
|
const third = s.object({ type: s.string({ default: "1" }), what22: s.string() });
|
||||||
registry.add("third", {
|
registry.add("third", {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
cls: NotAllowed,
|
cls: NotAllowed,
|
||||||
@@ -57,7 +56,7 @@ describe("Registry", () => {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(registry.get("third").schema).toEqual(third);
|
expect(registry.get("third").schema).toEqual(third);
|
||||||
|
|
||||||
const fourth = Type.Object({ type: Type.Number(), what22: Type.String() });
|
const fourth = s.object({ type: s.number(), what22: s.string() });
|
||||||
registry.add("fourth", {
|
registry.add("fourth", {
|
||||||
cls: What,
|
cls: What,
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
@@ -82,6 +81,8 @@ describe("Registry", () => {
|
|||||||
registry.register("what2", What2);
|
registry.register("what2", What2);
|
||||||
expect(registry.get("what2")).toBeDefined();
|
expect(registry.get("what2")).toBeDefined();
|
||||||
expect(registry.get("what2").cls).toBe(What2);
|
expect(registry.get("what2").cls).toBe(What2);
|
||||||
expect(registry.get("what2").schema).toEqual(What2.prototype.getType());
|
expect(JSON.stringify(registry.get("what2").schema)).toEqual(
|
||||||
|
JSON.stringify(What2.prototype.getType()),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
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
@@ -1,15 +0,0 @@
|
|||||||
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
@@ -1,84 +0,0 @@
|
|||||||
//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);
|
|
||||||
});*/
|
|
||||||
}
|
|
||||||
@@ -14,6 +14,7 @@ describe("env", () => {
|
|||||||
expect(is_toggled(1)).toBe(true);
|
expect(is_toggled(1)).toBe(true);
|
||||||
expect(is_toggled(0)).toBe(false);
|
expect(is_toggled(0)).toBe(false);
|
||||||
expect(is_toggled("anything else")).toBe(false);
|
expect(is_toggled("anything else")).toBe(false);
|
||||||
|
expect(is_toggled(undefined, true)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("env()", () => {
|
test("env()", () => {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { SchemaObject } from "../../../src/core";
|
import { SchemaObject } from "../../../src/core";
|
||||||
import { Type } from "../../../src/core/utils";
|
import { s } from "bknd/core";
|
||||||
|
|
||||||
describe("SchemaObject", async () => {
|
describe("SchemaObject", async () => {
|
||||||
test("basic", async () => {
|
test("basic", async () => {
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
Type.Object({ a: Type.String({ default: "b" }) }),
|
s.strictObject({ a: s.string({ default: "b" }) }),
|
||||||
{ a: "test" },
|
{ a: "test" },
|
||||||
{
|
{
|
||||||
forceParse: true,
|
forceParse: true,
|
||||||
@@ -23,19 +23,19 @@ describe("SchemaObject", async () => {
|
|||||||
|
|
||||||
test("patch", async () => {
|
test("patch", async () => {
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
Type.Object({
|
s.strictObject({
|
||||||
s: Type.Object(
|
s: s.strictObject(
|
||||||
{
|
{
|
||||||
a: Type.String({ default: "b" }),
|
a: s.string({ default: "b" }),
|
||||||
b: Type.Object(
|
b: s.strictObject(
|
||||||
{
|
{
|
||||||
c: Type.String({ default: "d" }),
|
c: s.string({ default: "d" }),
|
||||||
e: Type.String({ default: "f" }),
|
e: s.string({ default: "f" }),
|
||||||
},
|
},
|
||||||
{ default: {} },
|
{ default: {} },
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{ default: {}, additionalProperties: false },
|
{ default: {} },
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -44,7 +44,7 @@ describe("SchemaObject", async () => {
|
|||||||
await m.patch("s.a", "c");
|
await m.patch("s.a", "c");
|
||||||
|
|
||||||
// non-existing path on no additional properties
|
// non-existing path on no additional properties
|
||||||
expect(() => m.patch("s.s.s", "c")).toThrow();
|
expect(m.patch("s.s.s", "c")).rejects.toThrow();
|
||||||
// wrong type
|
// wrong type
|
||||||
expect(() => m.patch("s.a", 1)).toThrow();
|
expect(() => m.patch("s.a", 1)).toThrow();
|
||||||
|
|
||||||
@@ -58,8 +58,8 @@ describe("SchemaObject", async () => {
|
|||||||
|
|
||||||
test("patch array", async () => {
|
test("patch array", async () => {
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
Type.Object({
|
s.strictObject({
|
||||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
|
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
|
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
|
||||||
@@ -75,13 +75,13 @@ describe("SchemaObject", async () => {
|
|||||||
|
|
||||||
test("remove", async () => {
|
test("remove", async () => {
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
Type.Object({
|
s.object({
|
||||||
s: Type.Object(
|
s: s.object(
|
||||||
{
|
{
|
||||||
a: Type.String({ default: "b" }),
|
a: s.string({ default: "b" }),
|
||||||
b: Type.Object(
|
b: s.object(
|
||||||
{
|
{
|
||||||
c: Type.String({ default: "d" }),
|
c: s.string({ default: "d" }),
|
||||||
},
|
},
|
||||||
{ default: {} },
|
{ default: {} },
|
||||||
),
|
),
|
||||||
@@ -107,8 +107,8 @@ describe("SchemaObject", async () => {
|
|||||||
|
|
||||||
test("set", async () => {
|
test("set", async () => {
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
Type.Object({
|
s.strictObject({
|
||||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
|
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
|
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
|
||||||
@@ -124,8 +124,8 @@ describe("SchemaObject", async () => {
|
|||||||
let called = false;
|
let called = false;
|
||||||
let result: any;
|
let result: any;
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
Type.Object({
|
s.strictObject({
|
||||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
|
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
||||||
}),
|
}),
|
||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
@@ -145,8 +145,8 @@ describe("SchemaObject", async () => {
|
|||||||
test("listener: onBeforeUpdate", async () => {
|
test("listener: onBeforeUpdate", async () => {
|
||||||
let called = false;
|
let called = false;
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
Type.Object({
|
s.strictObject({
|
||||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
|
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
||||||
}),
|
}),
|
||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
@@ -167,7 +167,7 @@ describe("SchemaObject", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("throwIfRestricted", async () => {
|
test("throwIfRestricted", async () => {
|
||||||
const m = new SchemaObject(Type.Object({}), undefined, {
|
const m = new SchemaObject(s.strictObject({}), undefined, {
|
||||||
restrictPaths: ["a.b"],
|
restrictPaths: ["a.b"],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -179,13 +179,13 @@ describe("SchemaObject", async () => {
|
|||||||
|
|
||||||
test("restriction bypass", async () => {
|
test("restriction bypass", async () => {
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
Type.Object({
|
s.strictObject({
|
||||||
s: Type.Object(
|
s: s.strictObject(
|
||||||
{
|
{
|
||||||
a: Type.String({ default: "b" }),
|
a: s.string({ default: "b" }),
|
||||||
b: Type.Object(
|
b: s.strictObject(
|
||||||
{
|
{
|
||||||
c: Type.String({ default: "d" }),
|
c: s.string({ default: "d" }),
|
||||||
},
|
},
|
||||||
{ default: {} },
|
{ default: {} },
|
||||||
),
|
),
|
||||||
@@ -205,7 +205,21 @@ describe("SchemaObject", async () => {
|
|||||||
expect(m.get()).toEqual({ s: { a: "b", b: { c: "e" } } });
|
expect(m.get()).toEqual({ s: { a: "b", b: { c: "e" } } });
|
||||||
});
|
});
|
||||||
|
|
||||||
const dataEntitiesSchema = Type.Object(
|
const dataEntitiesSchema = s.strictObject({
|
||||||
|
entities: s.record(
|
||||||
|
s.object({
|
||||||
|
fields: s.record(
|
||||||
|
s.object({
|
||||||
|
type: s.string(),
|
||||||
|
config: s.object({}).optional(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
config: s.record(s.string()).optional(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
/* const dataEntitiesSchema = Type.Object(
|
||||||
{
|
{
|
||||||
entities: Type.Object(
|
entities: Type.Object(
|
||||||
{},
|
{},
|
||||||
@@ -230,7 +244,7 @@ describe("SchemaObject", async () => {
|
|||||||
{
|
{
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
},
|
},
|
||||||
);
|
); */
|
||||||
test("patch safe object, overwrite", async () => {
|
test("patch safe object, overwrite", async () => {
|
||||||
const data = {
|
const data = {
|
||||||
entities: {
|
entities: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { type ObjectQuery, convert, validate } from "../../../src/core/object/query/object-query";
|
import { type ObjectQuery, convert, validate } from "core/object/query/object-query";
|
||||||
|
|
||||||
describe("object-query", () => {
|
describe("object-query", () => {
|
||||||
const q: ObjectQuery = { name: "Michael" };
|
const q: ObjectQuery = { name: "Michael" };
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Perf, datetimeStringUTC, isBlob, ucFirst } from "../../src/core/utils";
|
import { Perf, 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) => {
|
||||||
@@ -75,57 +76,6 @@ 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 () => {
|
||||||
@@ -246,6 +196,76 @@ describe("Core Utils", async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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.only("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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("dates", () => {
|
describe("dates", () => {
|
||||||
test.only("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");
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
import { Guard } from "../../src/auth";
|
import { Guard } from "../../src/auth";
|
||||||
import { parse } from "../../src/core/utils";
|
import { parse } from "bknd/core";
|
||||||
import {
|
import {
|
||||||
Entity,
|
Entity,
|
||||||
type EntityData,
|
type EntityData,
|
||||||
EntityManager,
|
EntityManager,
|
||||||
ManyToOneRelation,
|
ManyToOneRelation,
|
||||||
type MutatorResponse,
|
|
||||||
type RepositoryResponse,
|
|
||||||
TextField,
|
TextField,
|
||||||
} from "../../src/data";
|
} from "../../src/data";
|
||||||
import { DataController } from "../../src/data/api/DataController";
|
import { DataController } from "../../src/data/api/DataController";
|
||||||
import { dataConfigSchema } from "../../src/data/data-schema";
|
import { dataConfigSchema } from "../../src/data/data-schema";
|
||||||
import { disableConsoleLog, enableConsoleLog, getDummyConnection } from "../helper";
|
import { disableConsoleLog, enableConsoleLog, getDummyConnection } from "../helper";
|
||||||
|
import type { RepositoryResultJSON } from "data/entities/query/RepositoryResult";
|
||||||
|
import type { MutatorResultJSON } from "data/entities/mutation/MutatorResult";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
beforeAll(() => disableConsoleLog(["log", "warn"]));
|
beforeAll(() => disableConsoleLog(["log", "warn"]));
|
||||||
@@ -21,52 +21,6 @@ afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
|||||||
|
|
||||||
const dataConfig = parse(dataConfigSchema, {});
|
const dataConfig = parse(dataConfigSchema, {});
|
||||||
describe("[data] DataController", async () => {
|
describe("[data] DataController", async () => {
|
||||||
test("repoResult", async () => {
|
|
||||||
const em = new EntityManager<any>([], dummyConnection);
|
|
||||||
const ctx: any = { em, guard: new Guard() };
|
|
||||||
const controller = new DataController(ctx, dataConfig);
|
|
||||||
|
|
||||||
const res = controller.repoResult({
|
|
||||||
entity: null as any,
|
|
||||||
data: [] as any,
|
|
||||||
sql: "",
|
|
||||||
parameters: [] as any,
|
|
||||||
result: [] as any,
|
|
||||||
meta: {
|
|
||||||
total: 0,
|
|
||||||
count: 0,
|
|
||||||
items: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res).toEqual({
|
|
||||||
meta: {
|
|
||||||
total: 0,
|
|
||||||
count: 0,
|
|
||||||
items: 0,
|
|
||||||
},
|
|
||||||
data: [],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("mutatorResult", async () => {
|
|
||||||
const em = new EntityManager([], dummyConnection);
|
|
||||||
const ctx: any = { em, guard: new Guard() };
|
|
||||||
const controller = new DataController(ctx, dataConfig);
|
|
||||||
|
|
||||||
const res = controller.mutatorResult({
|
|
||||||
entity: null as any,
|
|
||||||
data: [] as any,
|
|
||||||
sql: "",
|
|
||||||
parameters: [] as any,
|
|
||||||
result: [] as any,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res).toEqual({
|
|
||||||
data: [],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("getController", async () => {
|
describe("getController", async () => {
|
||||||
const users = new Entity("users", [
|
const users = new Entity("users", [
|
||||||
new TextField("name", { required: true }),
|
new TextField("name", { required: true }),
|
||||||
@@ -120,8 +74,7 @@ describe("[data] DataController", async () => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(_user),
|
body: JSON.stringify(_user),
|
||||||
});
|
});
|
||||||
//console.log("res", { _user }, res);
|
const result = (await res.json()) as MutatorResultJSON;
|
||||||
const result = (await res.json()) as MutatorResponse;
|
|
||||||
const { id, ...data } = result.data as any;
|
const { id, ...data } = result.data as any;
|
||||||
|
|
||||||
expect(res.status).toBe(201);
|
expect(res.status).toBe(201);
|
||||||
@@ -135,7 +88,7 @@ describe("[data] DataController", async () => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(_post),
|
body: JSON.stringify(_post),
|
||||||
});
|
});
|
||||||
const result = (await res.json()) as MutatorResponse;
|
const result = (await res.json()) as MutatorResultJSON;
|
||||||
const { id, ...data } = result.data as any;
|
const { id, ...data } = result.data as any;
|
||||||
|
|
||||||
expect(res.status).toBe(201);
|
expect(res.status).toBe(201);
|
||||||
@@ -146,13 +99,13 @@ describe("[data] DataController", async () => {
|
|||||||
|
|
||||||
test("/:entity (read many)", async () => {
|
test("/:entity (read many)", async () => {
|
||||||
const res = await app.request("/entity/users");
|
const res = await app.request("/entity/users");
|
||||||
const data = (await res.json()) as RepositoryResponse;
|
const data = (await res.json()) as RepositoryResultJSON;
|
||||||
|
|
||||||
expect(data.meta.total).toBe(3);
|
//expect(data.meta.total).toBe(3);
|
||||||
expect(data.meta.count).toBe(3);
|
//expect(data.meta.count).toBe(3);
|
||||||
expect(data.meta.items).toBe(3);
|
expect(data.meta.items).toBe(3);
|
||||||
expect(data.data.length).toBe(3);
|
expect(data.data.length).toBe(3);
|
||||||
expect(data.data[0].name).toBe("foo");
|
expect(data.data[0]?.name).toBe("foo");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("/:entity/query (func query)", async () => {
|
test("/:entity/query (func query)", async () => {
|
||||||
@@ -165,33 +118,32 @@ describe("[data] DataController", async () => {
|
|||||||
where: { bio: { $isnull: 1 } },
|
where: { bio: { $isnull: 1 } },
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const data = (await res.json()) as RepositoryResponse;
|
const data = (await res.json()) as RepositoryResultJSON;
|
||||||
|
|
||||||
expect(data.meta.total).toBe(3);
|
//expect(data.meta.total).toBe(3);
|
||||||
expect(data.meta.count).toBe(1);
|
//expect(data.meta.count).toBe(1);
|
||||||
expect(data.meta.items).toBe(1);
|
expect(data.meta.items).toBe(1);
|
||||||
expect(data.data.length).toBe(1);
|
expect(data.data.length).toBe(1);
|
||||||
expect(data.data[0].name).toBe("bar");
|
expect(data.data[0]?.name).toBe("bar");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("/:entity (read many, paginated)", async () => {
|
test("/:entity (read many, paginated)", async () => {
|
||||||
const res = await app.request("/entity/users?limit=1&offset=2");
|
const res = await app.request("/entity/users?limit=1&offset=2");
|
||||||
const data = (await res.json()) as RepositoryResponse;
|
const data = (await res.json()) as RepositoryResultJSON;
|
||||||
|
|
||||||
expect(data.meta.total).toBe(3);
|
//expect(data.meta.total).toBe(3);
|
||||||
expect(data.meta.count).toBe(3);
|
//expect(data.meta.count).toBe(3);
|
||||||
expect(data.meta.items).toBe(1);
|
expect(data.meta.items).toBe(1);
|
||||||
expect(data.data.length).toBe(1);
|
expect(data.data.length).toBe(1);
|
||||||
expect(data.data[0].name).toBe("baz");
|
expect(data.data[0]?.name).toBe("baz");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("/:entity/:id (read one)", async () => {
|
test("/:entity/:id (read one)", async () => {
|
||||||
const res = await app.request("/entity/users/3");
|
const res = await app.request("/entity/users/3");
|
||||||
const data = (await res.json()) as RepositoryResponse<EntityData>;
|
const data = (await res.json()) as RepositoryResultJSON<EntityData>;
|
||||||
console.log("data", data);
|
|
||||||
|
|
||||||
expect(data.meta.total).toBe(3);
|
//expect(data.meta.total).toBe(3);
|
||||||
expect(data.meta.count).toBe(1);
|
//expect(data.meta.count).toBe(1);
|
||||||
expect(data.meta.items).toBe(1);
|
expect(data.meta.items).toBe(1);
|
||||||
expect(data.data).toEqual({ id: 3, ...fixtures.users[2] });
|
expect(data.data).toEqual({ id: 3, ...fixtures.users[2] });
|
||||||
});
|
});
|
||||||
@@ -201,7 +153,7 @@ describe("[data] DataController", async () => {
|
|||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: JSON.stringify({ name: "new name" }),
|
body: JSON.stringify({ name: "new name" }),
|
||||||
});
|
});
|
||||||
const { data } = (await res.json()) as MutatorResponse;
|
const { data } = (await res.json()) as MutatorResultJSON;
|
||||||
|
|
||||||
expect(res.ok).toBe(true);
|
expect(res.ok).toBe(true);
|
||||||
expect(data as any).toEqual({ id: 3, ...fixtures.users[2], name: "new name" });
|
expect(data as any).toEqual({ id: 3, ...fixtures.users[2], name: "new name" });
|
||||||
@@ -209,27 +161,26 @@ describe("[data] DataController", async () => {
|
|||||||
|
|
||||||
test("/:entity/:id/:reference (read references)", async () => {
|
test("/:entity/:id/:reference (read references)", async () => {
|
||||||
const res = await app.request("/entity/users/1/posts");
|
const res = await app.request("/entity/users/1/posts");
|
||||||
const data = (await res.json()) as RepositoryResponse;
|
const data = (await res.json()) as RepositoryResultJSON;
|
||||||
console.log("data", data);
|
|
||||||
|
|
||||||
expect(data.meta.total).toBe(2);
|
//expect(data.meta.total).toBe(2);
|
||||||
expect(data.meta.count).toBe(1);
|
//expect(data.meta.count).toBe(1);
|
||||||
expect(data.meta.items).toBe(1);
|
expect(data.meta.items).toBe(1);
|
||||||
expect(data.data.length).toBe(1);
|
expect(data.data.length).toBe(1);
|
||||||
expect(data.data[0].content).toBe("post 1");
|
expect(data.data[0]?.content).toBe("post 1");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("/:entity/:id (delete one)", async () => {
|
test("/:entity/:id (delete one)", async () => {
|
||||||
const res = await app.request("/entity/posts/2", {
|
const res = await app.request("/entity/posts/2", {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
const { data } = (await res.json()) as RepositoryResponse<EntityData>;
|
const { data } = (await res.json()) as RepositoryResultJSON<EntityData>;
|
||||||
expect(data).toEqual({ id: 2, ...fixtures.posts[1] });
|
expect(data).toEqual({ id: 2, ...fixtures.posts[1] });
|
||||||
|
|
||||||
// verify
|
// verify
|
||||||
const res2 = await app.request("/entity/posts");
|
const res2 = await app.request("/entity/posts");
|
||||||
const data2 = (await res2.json()) as RepositoryResponse;
|
const data2 = (await res2.json()) as RepositoryResultJSON;
|
||||||
expect(data2.meta.total).toBe(1);
|
//expect(data2.meta.total).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,19 +34,12 @@ describe("some tests", async () => {
|
|||||||
|
|
||||||
test("findId", async () => {
|
test("findId", async () => {
|
||||||
const query = await em.repository(users).findId(1);
|
const query = await em.repository(users).findId(1);
|
||||||
/*const { result, total, count, time } = query;
|
|
||||||
console.log("query", query.result, {
|
|
||||||
result,
|
|
||||||
total,
|
|
||||||
count,
|
|
||||||
time,
|
|
||||||
});*/
|
|
||||||
|
|
||||||
expect(query.sql).toBe(
|
expect(query.sql).toBe(
|
||||||
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" where "id" = ? limit ?',
|
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" where "id" = ? limit ?',
|
||||||
);
|
);
|
||||||
expect(query.parameters).toEqual([1, 1]);
|
expect(query.parameters).toEqual([1, 1]);
|
||||||
expect(query.result).toEqual([]);
|
expect(query.data).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("findMany", async () => {
|
test("findMany", async () => {
|
||||||
@@ -56,7 +49,7 @@ describe("some tests", async () => {
|
|||||||
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" order by "users"."id" asc limit ? offset ?',
|
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" order by "users"."id" asc limit ? offset ?',
|
||||||
);
|
);
|
||||||
expect(query.parameters).toEqual([10, 0]);
|
expect(query.parameters).toEqual([10, 0]);
|
||||||
expect(query.result).toEqual([]);
|
expect(query.data).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("findMany with number", async () => {
|
test("findMany with number", async () => {
|
||||||
@@ -66,7 +59,7 @@ describe("some tests", async () => {
|
|||||||
'select "posts"."id" as "id", "posts"."title" as "title", "posts"."content" as "content", "posts"."created_at" as "created_at", "posts"."likes" as "likes" from "posts" order by "posts"."id" asc limit ? offset ?',
|
'select "posts"."id" as "id", "posts"."title" as "title", "posts"."content" as "content", "posts"."created_at" as "created_at", "posts"."likes" as "likes" from "posts" order by "posts"."id" asc limit ? offset ?',
|
||||||
);
|
);
|
||||||
expect(query.parameters).toEqual([10, 0]);
|
expect(query.parameters).toEqual([10, 0]);
|
||||||
expect(query.result).toEqual([]);
|
expect(query.data).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("try adding an existing field name", async () => {
|
test("try adding an existing field name", async () => {
|
||||||
@@ -110,4 +103,18 @@ 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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ describe("Mutator simple", async () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(query.result).toEqual([{ id: 1, label: "test", count: 1 }]);
|
expect(query.data).toEqual([{ id: 1, label: "test", count: 1 }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("update inserted row", async () => {
|
test("update inserted row", async () => {
|
||||||
@@ -87,7 +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.result.length).toBe(0);
|
expect(query2.data).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("validation: insert incomplete row", async () => {
|
test("validation: insert incomplete row", async () => {
|
||||||
@@ -177,13 +177,13 @@ describe("Mutator simple", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("insertMany", async () => {
|
test("insertMany", async () => {
|
||||||
const oldCount = (await em.repo(items).count()).count;
|
const oldCount = (await em.repo(items).count()).data.count;
|
||||||
const inserts = [{ label: "insert 1" }, { label: "insert 2" }];
|
const inserts = [{ label: "insert 1" }, { label: "insert 2" }];
|
||||||
const { data } = await em.mutator(items).insertMany(inserts);
|
const { data } = await em.mutator(items).insertMany(inserts);
|
||||||
|
|
||||||
expect(data.length).toBe(2);
|
expect(data.length).toBe(2);
|
||||||
expect(data.map((d) => ({ label: d.label }))).toEqual(inserts);
|
expect(data.map((d) => ({ label: d.label }))).toEqual(inserts);
|
||||||
const newCount = (await em.repo(items).count()).count;
|
const newCount = (await em.repo(items).count()).data.count;
|
||||||
expect(newCount).toBe(oldCount + inserts.length);
|
expect(newCount).toBe(oldCount + inserts.length);
|
||||||
|
|
||||||
const { data: data2 } = await em.repo(items).findMany({ offset: oldCount });
|
const { data: data2 } = await em.repo(items).findMany({ offset: oldCount });
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { afterAll, expect as bunExpect, describe, test } from "bun:test";
|
import { afterAll, expect as bunExpect, describe, test } from "bun:test";
|
||||||
import { stripMark } from "../../src/core/utils";
|
import { stripMark } from "bknd/core";
|
||||||
import { Entity, EntityManager, PolymorphicRelation, TextField } from "../../src/data";
|
import { Entity, EntityManager, PolymorphicRelation, TextField } from "../../src/data";
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,8 @@ describe("prototype", () => {
|
|||||||
|
|
||||||
type Posts = Schema<typeof posts2>;
|
type Posts = Schema<typeof posts2>;
|
||||||
|
|
||||||
expect(posts1.toJSON()).toEqual(posts2.toJSON());
|
// @todo: check
|
||||||
|
//expect(posts1.toJSON()).toEqual(posts2.toJSON());
|
||||||
});
|
});
|
||||||
|
|
||||||
test("test example", async () => {
|
test("test example", async () => {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ describe("Relations", async () => {
|
|||||||
|
|
||||||
const sql1 = schema
|
const sql1 = schema
|
||||||
.createTable("posts")
|
.createTable("posts")
|
||||||
.addColumn(...r1.schema()!)
|
.addColumn(...em.connection.getFieldSchema(r1.schema())!)
|
||||||
.compile().sql;
|
.compile().sql;
|
||||||
|
|
||||||
expect(sql1).toBe(
|
expect(sql1).toBe(
|
||||||
@@ -43,7 +43,7 @@ describe("Relations", async () => {
|
|||||||
|
|
||||||
const sql2 = schema
|
const sql2 = schema
|
||||||
.createTable("posts")
|
.createTable("posts")
|
||||||
.addColumn(...r2.schema()!)
|
.addColumn(...em.connection.getFieldSchema(r2.schema())!)
|
||||||
.compile().sql;
|
.compile().sql;
|
||||||
|
|
||||||
expect(sql2).toBe(
|
expect(sql2).toBe(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Entity, NumberField, TextField } from "../../../src/data";
|
import { Entity, NumberField, TextField } from "data";
|
||||||
|
import * as p from "data/prototype";
|
||||||
|
|
||||||
describe("[data] Entity", async () => {
|
describe("[data] Entity", async () => {
|
||||||
const entity = new Entity("test", [
|
const entity = new Entity("test", [
|
||||||
@@ -47,14 +48,7 @@ describe("[data] Entity", async () => {
|
|||||||
expect(entity.getField("new_field")).toBe(field);
|
expect(entity.getField("new_field")).toBe(field);
|
||||||
});
|
});
|
||||||
|
|
||||||
// @todo: move this to ClientApp
|
test.only("types", async () => {
|
||||||
/*test("serialize and deserialize", async () => {
|
console.log(entity.toTypes());
|
||||||
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);
|
|
||||||
});*/
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,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,4 +1,4 @@
|
|||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import type { EventManager } from "../../../src/core/events";
|
import type { EventManager } from "../../../src/core/events";
|
||||||
import {
|
import {
|
||||||
Entity,
|
Entity,
|
||||||
@@ -12,11 +12,14 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
} from "../../../src/data";
|
} from "../../../src/data";
|
||||||
import * as proto from "../../../src/data/prototype";
|
import * as proto from "../../../src/data/prototype";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection, disableConsoleLog, enableConsoleLog } from "../../helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
afterAll(afterAllCleanup);
|
afterAll(afterAllCleanup);
|
||||||
|
|
||||||
|
beforeAll(() => disableConsoleLog(["log", "warn"]));
|
||||||
|
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||||
|
|
||||||
describe("[data] Mutator (base)", async () => {
|
describe("[data] Mutator (base)", async () => {
|
||||||
const entity = new Entity("items", [
|
const entity = new Entity("items", [
|
||||||
new TextField("label", { required: true }),
|
new TextField("label", { required: true }),
|
||||||
@@ -288,14 +291,17 @@ 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,6 +1,6 @@
|
|||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import type { Kysely, Transaction } from "kysely";
|
import type { Kysely, Transaction } from "kysely";
|
||||||
import { Perf } from "../../../src/core/utils";
|
import { Perf } from "core/utils";
|
||||||
import {
|
import {
|
||||||
Entity,
|
Entity,
|
||||||
EntityManager,
|
EntityManager,
|
||||||
@@ -8,7 +8,10 @@ import {
|
|||||||
ManyToOneRelation,
|
ManyToOneRelation,
|
||||||
RepositoryEvents,
|
RepositoryEvents,
|
||||||
TextField,
|
TextField,
|
||||||
} from "../../../src/data";
|
entity as $entity,
|
||||||
|
text as $text,
|
||||||
|
em as $em,
|
||||||
|
} from "data";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
|
|
||||||
type E = Kysely<any> | Transaction<any>;
|
type E = Kysely<any> | Transaction<any>;
|
||||||
@@ -23,120 +26,6 @@ async function sleep(ms: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("[Repository]", async () => {
|
describe("[Repository]", async () => {
|
||||||
test.skip("bulk", async () => {
|
|
||||||
//const connection = dummyConnection;
|
|
||||||
//const connection = getLocalLibsqlConnection();
|
|
||||||
const credentials = null as any; // @todo: determine what to do here
|
|
||||||
const connection = new LibsqlConnection(credentials);
|
|
||||||
|
|
||||||
const em = new EntityManager([], connection);
|
|
||||||
/*const emLibsql = new EntityManager([], {
|
|
||||||
url: connection.url.replace("https", "libsql"),
|
|
||||||
authToken: connection.authToken,
|
|
||||||
});*/
|
|
||||||
const table = "posts";
|
|
||||||
|
|
||||||
const client = connection.getClient();
|
|
||||||
if (!client) {
|
|
||||||
console.log("Cannot perform test without libsql connection");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const conn = em.connection.kysely;
|
|
||||||
const selectQ = (e: E) => e.selectFrom(table).selectAll().limit(2);
|
|
||||||
const countQ = (e: E) => e.selectFrom(table).select(e.fn.count("*").as("count"));
|
|
||||||
|
|
||||||
async function executeTransaction(em: EntityManager<any>) {
|
|
||||||
return await em.connection.kysely.transaction().execute(async (e) => {
|
|
||||||
const res = await selectQ(e).execute();
|
|
||||||
const count = await countQ(e).execute();
|
|
||||||
|
|
||||||
return [res, count];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function executeBatch(em: EntityManager<any>) {
|
|
||||||
const queries = [selectQ(conn), countQ(conn)];
|
|
||||||
return await em.connection.batchQuery(queries);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function executeSingleKysely(em: EntityManager<any>) {
|
|
||||||
const res = await selectQ(conn).execute();
|
|
||||||
const count = await countQ(conn).execute();
|
|
||||||
return [res, count];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function executeSingleClient(em: EntityManager<any>) {
|
|
||||||
const q1 = selectQ(conn).compile();
|
|
||||||
const res = await client.execute({
|
|
||||||
sql: q1.sql,
|
|
||||||
args: q1.parameters as any,
|
|
||||||
});
|
|
||||||
|
|
||||||
const q2 = countQ(conn).compile();
|
|
||||||
const count = await client.execute({
|
|
||||||
sql: q2.sql,
|
|
||||||
args: q2.parameters as any,
|
|
||||||
});
|
|
||||||
return [res, count];
|
|
||||||
}
|
|
||||||
|
|
||||||
const transaction = await executeTransaction(em);
|
|
||||||
const batch = await executeBatch(em);
|
|
||||||
|
|
||||||
expect(batch).toEqual(transaction as any);
|
|
||||||
|
|
||||||
const testperf = false;
|
|
||||||
if (testperf) {
|
|
||||||
const times = 5;
|
|
||||||
|
|
||||||
const exec = async (
|
|
||||||
name: string,
|
|
||||||
fn: (em: EntityManager<any>) => Promise<any>,
|
|
||||||
em: EntityManager<any>,
|
|
||||||
) => {
|
|
||||||
const res = await Perf.execute(() => fn(em), times);
|
|
||||||
await sleep(1000);
|
|
||||||
const info = {
|
|
||||||
name,
|
|
||||||
total: res.total.toFixed(2),
|
|
||||||
avg: (res.total / times).toFixed(2),
|
|
||||||
first: res.marks[0].time.toFixed(2),
|
|
||||||
last: res.marks[res.marks.length - 1].time.toFixed(2),
|
|
||||||
};
|
|
||||||
console.log(info.name, info, res.marks);
|
|
||||||
return info;
|
|
||||||
};
|
|
||||||
|
|
||||||
const data: any[] = [];
|
|
||||||
data.push(await exec("transaction.http", executeTransaction, em));
|
|
||||||
data.push(await exec("bulk.http", executeBatch, em));
|
|
||||||
data.push(await exec("singleKy.http", executeSingleKysely, em));
|
|
||||||
data.push(await exec("singleCl.http", executeSingleClient, em));
|
|
||||||
|
|
||||||
/*data.push(await exec("transaction.libsql", executeTransaction, emLibsql));
|
|
||||||
data.push(await exec("bulk.libsql", executeBatch, emLibsql));
|
|
||||||
data.push(await exec("singleKy.libsql", executeSingleKysely, emLibsql));
|
|
||||||
data.push(await exec("singleCl.libsql", executeSingleClient, emLibsql));*/
|
|
||||||
|
|
||||||
console.table(data);
|
|
||||||
/**
|
|
||||||
* ┌───┬────────────────────┬────────┬────────┬────────┬────────┐
|
|
||||||
* │ │ name │ total │ avg │ first │ last │
|
|
||||||
* ├───┼────────────────────┼────────┼────────┼────────┼────────┤
|
|
||||||
* │ 0 │ transaction.http │ 681.29 │ 136.26 │ 136.46 │ 396.09 │
|
|
||||||
* │ 1 │ bulk.http │ 164.82 │ 32.96 │ 32.95 │ 99.91 │
|
|
||||||
* │ 2 │ singleKy.http │ 330.01 │ 66.00 │ 65.86 │ 195.41 │
|
|
||||||
* │ 3 │ singleCl.http │ 326.17 │ 65.23 │ 61.32 │ 198.08 │
|
|
||||||
* │ 4 │ transaction.libsql │ 856.79 │ 171.36 │ 132.31 │ 595.24 │
|
|
||||||
* │ 5 │ bulk.libsql │ 180.63 │ 36.13 │ 35.39 │ 107.71 │
|
|
||||||
* │ 6 │ singleKy.libsql │ 347.11 │ 69.42 │ 65.00 │ 207.14 │
|
|
||||||
* │ 7 │ singleCl.libsql │ 328.60 │ 65.72 │ 62.19 │ 195.04 │
|
|
||||||
* └───┴────────────────────┴────────┴────────┴────────┴────────┘
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("count & exists", async () => {
|
test("count & exists", async () => {
|
||||||
const items = new Entity("items", [new TextField("label")]);
|
const items = new Entity("items", [new TextField("label")]);
|
||||||
const em = new EntityManager([items], dummyConnection);
|
const em = new EntityManager([items], dummyConnection);
|
||||||
@@ -157,25 +46,88 @@ describe("[Repository]", async () => {
|
|||||||
// count all
|
// count all
|
||||||
const res = await em.repository(items).count();
|
const res = await em.repository(items).count();
|
||||||
expect(res.sql).toBe('select count(*) as "count" from "items"');
|
expect(res.sql).toBe('select count(*) as "count" from "items"');
|
||||||
expect(res.count).toBe(3);
|
expect(res.data.count).toBe(3);
|
||||||
|
|
||||||
|
//
|
||||||
|
{
|
||||||
|
const res = await em.repository(items).findMany();
|
||||||
|
expect(res.count).toBe(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const res = await em
|
||||||
|
.repository(items, {
|
||||||
|
includeCounts: true,
|
||||||
|
})
|
||||||
|
.findMany();
|
||||||
|
expect(res.count).toBe(3);
|
||||||
|
}
|
||||||
|
|
||||||
// count filtered
|
// count filtered
|
||||||
const res2 = await em.repository(items).count({ label: { $in: ["a", "b"] } });
|
const res2 = await em
|
||||||
|
.repository(items, {
|
||||||
|
includeCounts: true,
|
||||||
|
})
|
||||||
|
.count({ label: { $in: ["a", "b"] } });
|
||||||
|
|
||||||
expect(res2.sql).toBe('select count(*) as "count" from "items" where "label" in (?, ?)');
|
expect(res2.sql).toBe('select count(*) as "count" from "items" where "label" in (?, ?)');
|
||||||
expect(res2.parameters).toEqual(["a", "b"]);
|
expect(res2.parameters).toEqual(["a", "b"]);
|
||||||
expect(res2.count).toBe(2);
|
expect(res2.data.count).toBe(2);
|
||||||
|
|
||||||
// check exists
|
// check exists
|
||||||
const res3 = await em.repository(items).exists({ label: "a" });
|
const res3 = await em.repository(items).exists({ label: "a" });
|
||||||
expect(res3.exists).toBe(true);
|
expect(res3.data.exists).toBe(true);
|
||||||
|
|
||||||
const res4 = await em.repository(items).exists({ label: "d" });
|
const res4 = await em.repository(items).exists({ label: "d" });
|
||||||
expect(res4.exists).toBe(false);
|
expect(res4.data.exists).toBe(false);
|
||||||
|
|
||||||
// for now, allow empty filter
|
// for now, allow empty filter
|
||||||
const res5 = await em.repository(items).exists({});
|
const res5 = await em.repository(items).exists({});
|
||||||
expect(res5.exists).toBe(true);
|
expect(res5.data.exists).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("option: silent", async () => {
|
||||||
|
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]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -198,24 +150,36 @@ describe("[data] Repository (Events)", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("events were fired", async () => {
|
test("events were fired", async () => {
|
||||||
await em.repository(items).findId(1);
|
const repo = em.repository(items);
|
||||||
|
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 em.repository(items).findOne({ id: 1 });
|
await repo.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 em.repository(items).findMany({ where: { id: 1 } });
|
await repo.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 em.repository(items).findManyByReference(1, "categories");
|
await repo.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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ describe("SchemaManager tests", async () => {
|
|||||||
const em = new EntityManager([entity], dummyConnection, [], [index]);
|
const em = new EntityManager([entity], dummyConnection, [], [index]);
|
||||||
const schema = new SchemaManager(em);
|
const schema = new SchemaManager(em);
|
||||||
|
|
||||||
const introspection = schema.getIntrospectionFromEntity(em.entities[0]);
|
const introspection = schema.getIntrospectionFromEntity(em.entities[0]!);
|
||||||
expect(introspection).toEqual({
|
expect(introspection).toEqual({
|
||||||
name: "test",
|
name: "test",
|
||||||
isView: false,
|
isView: false,
|
||||||
@@ -109,7 +109,7 @@ describe("SchemaManager tests", async () => {
|
|||||||
await schema.sync({ force: true, drop: true });
|
await schema.sync({ force: true, drop: true });
|
||||||
const diffAfter = await schema.getDiff();
|
const diffAfter = await schema.getDiff();
|
||||||
|
|
||||||
console.log("diffAfter", diffAfter);
|
//console.log("diffAfter", diffAfter);
|
||||||
expect(diffAfter.length).toBe(0);
|
expect(diffAfter.length).toBe(0);
|
||||||
|
|
||||||
await kysely.schema.dropTable(table).execute();
|
await kysely.schema.dropTable(table).execute();
|
||||||
|
|||||||
+13
-80
@@ -1,25 +1,18 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, test, expect } from "bun:test";
|
||||||
import { Value, _jsonp } from "../../src/core/utils";
|
import { getDummyConnection } from "../helper";
|
||||||
import { type RepoQuery, WhereBuilder, type WhereQuery, querySchema } from "../../src/data";
|
import { type WhereQuery, WhereBuilder } from "data";
|
||||||
import type { RepoQueryIn } from "../../src/data/server/data-query-impl";
|
|
||||||
import { getDummyConnection } from "./helper";
|
|
||||||
|
|
||||||
const decode = (input: RepoQueryIn, expected: RepoQuery) => {
|
function qb() {
|
||||||
const result = Value.Decode(querySchema, input);
|
const c = getDummyConnection();
|
||||||
expect(result).toEqual(expected);
|
const kysely = c.dummyConnection.kysely;
|
||||||
};
|
return kysely.selectFrom("t").selectAll();
|
||||||
|
}
|
||||||
describe("data-query-impl", () => {
|
function compile(q: WhereQuery) {
|
||||||
function qb() {
|
const { sql, parameters } = WhereBuilder.addClause(qb(), q).compile();
|
||||||
const c = getDummyConnection();
|
return { sql, parameters };
|
||||||
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]],
|
||||||
@@ -94,64 +87,4 @@ describe("data-query-impl", () => {
|
|||||||
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" } });
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
@@ -89,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 ? offset ?) as obj) as "author" from "posts"',
|
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "users"."id" as "id", "users"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" order by "users"."id" asc limit ?) as obj) as "author" from "posts"',
|
||||||
);
|
);
|
||||||
expect(res2.parameters).toEqual([1, 0]);
|
expect(res2.parameters).toEqual([1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("test with empty join", async () => {
|
test("test with empty join", async () => {
|
||||||
@@ -194,9 +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 ? offset ?) as obj) as "single" from "categories"',
|
'select (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ?) as obj) as "single" from "categories"',
|
||||||
);
|
);
|
||||||
expect(res.parameters).toEqual(["categories.single", 1, 0]);
|
expect(res.parameters).toEqual(["categories.single", 1]);
|
||||||
|
|
||||||
const qb2 = WithBuilder.addClause(
|
const qb2 = WithBuilder.addClause(
|
||||||
em,
|
em,
|
||||||
@@ -273,9 +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 ? offset ?) as obj) as "avatar" from "users" as "users" where "users"."id" = "posts"."users_id" order by "users"."username" asc limit ? offset ?) as obj) as "users" from "posts"',
|
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username", \'avatar\', "obj"."avatar") from (select "users"."id" as "id", "users"."username" as "username", (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "users"."id" = "media"."entity_id" order by "media"."id" asc limit ?) as obj) as "avatar" from "users" as "users" where "users"."id" = "posts"."users_id" order by "users"."username" asc limit ?) as obj) as "users" from "posts"',
|
||||||
);
|
);
|
||||||
expect(qb.compile().parameters).toEqual(["users.avatar", 1, 0, 1, 0]);
|
expect(qb.compile().parameters).toEqual(["users.avatar", 1, 1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("compiles with many", async () => {
|
test("compiles with many", async () => {
|
||||||
@@ -315,9 +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 ? offset ?) as obj) as "users" from "comments" as "comments" where "comments"."posts_id" = "posts"."id" order by "comments"."id" asc limit ? offset ?) as agg) as "comments" from "posts"',
|
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'posts_id\', "agg"."posts_id", \'users_id\', "agg"."users_id", \'users\', "agg"."users")), \'[]\') from (select "comments"."id" as "id", "comments"."posts_id" as "posts_id", "comments"."users_id" as "users_id", (select json_object(\'username\', "obj"."username") from (select "users"."username" as "username" from "users" as "users" where "users"."id" = "comments"."users_id" order by "users"."id" asc limit ?) as obj) as "users" from "comments" as "comments" where "comments"."posts_id" = "posts"."id" order by "comments"."id" asc limit ? offset ?) as agg) as "comments" from "posts"',
|
||||||
);
|
);
|
||||||
expect(qb.compile().parameters).toEqual([1, 0, 12, 0]);
|
expect(qb.compile().parameters).toEqual([1, 12, 0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("returns correct result", async () => {
|
test("returns correct result", async () => {
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { SqliteIntrospector } from "data/connection";
|
||||||
|
import { getDummyDatabase } from "../../helper";
|
||||||
|
import { Kysely, SqliteDialect } from "kysely";
|
||||||
|
|
||||||
|
function create() {
|
||||||
|
const database = getDummyDatabase().dummyDb;
|
||||||
|
return new Kysely({
|
||||||
|
dialect: new SqliteDialect({ database }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLibsql() {
|
||||||
|
const database = getDummyDatabase().dummyDb;
|
||||||
|
return new Kysely({
|
||||||
|
dialect: new SqliteDialect({ database }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("SqliteIntrospector", () => {
|
||||||
|
test("asdf", async () => {
|
||||||
|
const kysely = create();
|
||||||
|
|
||||||
|
await kysely.schema
|
||||||
|
.createTable("test")
|
||||||
|
.addColumn("id", "integer", (col) => col.primaryKey().autoIncrement().notNull())
|
||||||
|
.addColumn("string", "text", (col) => col.notNull())
|
||||||
|
.addColumn("number", "integer")
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await kysely.schema
|
||||||
|
.createIndex("idx_test_string")
|
||||||
|
.on("test")
|
||||||
|
.columns(["string"])
|
||||||
|
.unique()
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await kysely.schema
|
||||||
|
.createTable("test2")
|
||||||
|
.addColumn("id", "integer", (col) => col.primaryKey().autoIncrement().notNull())
|
||||||
|
.addColumn("number", "integer")
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await kysely.schema.createIndex("idx_test2_number").on("test2").columns(["number"]).execute();
|
||||||
|
|
||||||
|
const introspector = new SqliteIntrospector(kysely, {});
|
||||||
|
|
||||||
|
const result = await introspector.getTables();
|
||||||
|
|
||||||
|
//console.log(_jsonp(result));
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
name: "test",
|
||||||
|
isView: false,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: "id",
|
||||||
|
dataType: "INTEGER",
|
||||||
|
isNullable: false,
|
||||||
|
isAutoIncrementing: true,
|
||||||
|
hasDefaultValue: false,
|
||||||
|
comment: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "string",
|
||||||
|
dataType: "TEXT",
|
||||||
|
isNullable: false,
|
||||||
|
isAutoIncrementing: false,
|
||||||
|
hasDefaultValue: false,
|
||||||
|
comment: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comment: undefined,
|
||||||
|
dataType: "INTEGER",
|
||||||
|
hasDefaultValue: false,
|
||||||
|
isAutoIncrementing: false,
|
||||||
|
isNullable: true,
|
||||||
|
name: "number",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test2",
|
||||||
|
isView: false,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: "id",
|
||||||
|
dataType: "INTEGER",
|
||||||
|
isNullable: false,
|
||||||
|
isAutoIncrementing: true,
|
||||||
|
hasDefaultValue: false,
|
||||||
|
comment: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "number",
|
||||||
|
dataType: "INTEGER",
|
||||||
|
isNullable: true,
|
||||||
|
isAutoIncrementing: false,
|
||||||
|
hasDefaultValue: false,
|
||||||
|
comment: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { BooleanField } from "../../../../src/data";
|
import { BooleanField } from "../../../../src/data";
|
||||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
describe("[data] BooleanField", async () => {
|
describe("[data] BooleanField", async () => {
|
||||||
runBaseFieldTests(BooleanField, { defaultValue: true, schemaType: "boolean" });
|
fieldTestSuite({ expect, test }, BooleanField, { defaultValue: true, schemaType: "boolean" });
|
||||||
|
|
||||||
test("transformRetrieve", async () => {
|
test("transformRetrieve", async () => {
|
||||||
const field = new BooleanField("test");
|
const field = new BooleanField("test");
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { DateField } from "../../../../src/data";
|
import { DateField, dateFieldConfigSchema } from "../../../../src/data";
|
||||||
import { runBaseFieldTests } from "./inc";
|
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||||
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
|
|
||||||
describe("[data] DateField", async () => {
|
describe("[data] DateField", async () => {
|
||||||
runBaseFieldTests(DateField, { defaultValue: new Date(), schemaType: "date" });
|
fieldTestSuite(
|
||||||
|
bunTestRunner,
|
||||||
|
DateField,
|
||||||
|
{ defaultValue: new Date(), schemaType: "date" },
|
||||||
|
{ type: "date" },
|
||||||
|
);
|
||||||
|
|
||||||
// @todo: add datefield tests
|
// @todo: add datefield tests
|
||||||
test("week", async () => {
|
test("week", async () => {
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { EnumField } from "../../../../src/data";
|
import { EnumField } from "../../../../src/data";
|
||||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
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 () => {
|
||||||
runBaseFieldTests(
|
fieldTestSuite(
|
||||||
|
{ expect, test },
|
||||||
|
// @ts-ignore
|
||||||
EnumField,
|
EnumField,
|
||||||
{ defaultValue: "a", schemaType: "text" },
|
{ defaultValue: "a", schemaType: "text" },
|
||||||
{ options: options(["a", "b", "c"]) },
|
{ options: options(["a", "b", "c"]) },
|
||||||
@@ -15,11 +17,13 @@ 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();
|
||||||
@@ -29,6 +33,7 @@ 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,23 +1,30 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Default, parse, stripMark } from "../../../../src/core/utils";
|
import { baseFieldConfigSchema, Field } from "../../../../src/data/fields/Field";
|
||||||
import { Field, type SchemaResponse, TextField, baseFieldConfigSchema } from "../../../../src/data";
|
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
|
import { stripMark } from "bknd/core";
|
||||||
|
|
||||||
describe("[data] Field", async () => {
|
describe("[data] Field", async () => {
|
||||||
class FieldSpec extends Field {
|
class FieldSpec extends Field {
|
||||||
schema(): SchemaResponse {
|
|
||||||
return this.useSchemaHelper("text");
|
|
||||||
}
|
|
||||||
getSchema() {
|
getSchema() {
|
||||||
return baseFieldConfigSchema;
|
return baseFieldConfigSchema;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
runBaseFieldTests(FieldSpec, { defaultValue: "test", schemaType: "text" });
|
test("fieldSpec", () => {
|
||||||
|
expect(new FieldSpec("test").schema()).toEqual({
|
||||||
|
name: "test",
|
||||||
|
type: "text",
|
||||||
|
nullable: true, // always true
|
||||||
|
dflt: undefined, // never using default value
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
fieldTestSuite(bunTestRunner, FieldSpec, { defaultValue: "test", schemaType: "text" });
|
||||||
|
|
||||||
test("default config", async () => {
|
test("default config", async () => {
|
||||||
const config = Default(baseFieldConfigSchema, {});
|
const config = baseFieldConfigSchema.template({});
|
||||||
expect(stripMark(new FieldSpec("test").config)).toEqual(config);
|
expect(stripMark(new FieldSpec("test").config)).toEqual(config as any);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("transformPersist (specific)", async () => {
|
test("transformPersist (specific)", async () => {
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Type } from "../../../../src/core/utils";
|
import { Entity, EntityIndex, Field } from "../../../../src/data";
|
||||||
import {
|
import { s } from "bknd/core";
|
||||||
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 Type.Any();
|
return s.any();
|
||||||
}
|
}
|
||||||
|
|
||||||
schema(em: EntityManager<any>): SchemaResponse {
|
override schema() {
|
||||||
return undefined as any;
|
return undefined as any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { JsonField } from "../../../../src/data";
|
import { JsonField } from "../../../../src/data";
|
||||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
describe("[data] JsonField", async () => {
|
describe("[data] JsonField", async () => {
|
||||||
const field = new JsonField("test");
|
const field = new JsonField("test");
|
||||||
runBaseFieldTests(JsonField, {
|
fieldTestSuite({ expect, test }, JsonField, {
|
||||||
defaultValue: { a: 1 },
|
defaultValue: { a: 1 },
|
||||||
sampleValues: ["string", { test: 1 }, 1],
|
sampleValues: ["string", { test: 1 }, 1],
|
||||||
schemaType: "text",
|
schemaType: "text",
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { JsonSchemaField } from "../../../../src/data";
|
import { JsonSchemaField } from "../../../../src/data";
|
||||||
import { runBaseFieldTests } from "./inc";
|
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
describe("[data] JsonSchemaField", async () => {
|
describe("[data] JsonSchemaField", async () => {
|
||||||
runBaseFieldTests(JsonSchemaField, { defaultValue: {}, schemaType: "text" });
|
// @ts-ignore
|
||||||
|
fieldTestSuite({ expect, test }, JsonSchemaField, { defaultValue: {}, schemaType: "text" });
|
||||||
|
|
||||||
// @todo: add JsonSchemaField tests
|
// @todo: add JsonSchemaField tests
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { NumberField } from "../../../../src/data";
|
import { NumberField } from "../../../../src/data";
|
||||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
describe("[data] NumberField", async () => {
|
describe("[data] NumberField", async () => {
|
||||||
test("transformPersist (config)", async () => {
|
test("transformPersist (config)", async () => {
|
||||||
@@ -15,5 +15,5 @@ describe("[data] NumberField", async () => {
|
|||||||
expect(transformPersist(field2, 10000)).resolves.toBe(10000);
|
expect(transformPersist(field2, 10000)).resolves.toBe(10000);
|
||||||
});
|
});
|
||||||
|
|
||||||
runBaseFieldTests(NumberField, { defaultValue: 12, schemaType: "integer" });
|
fieldTestSuite({ expect, test }, NumberField, { defaultValue: 12, schemaType: "integer" });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ describe("[data] PrimaryField", async () => {
|
|||||||
|
|
||||||
test("schema", () => {
|
test("schema", () => {
|
||||||
expect(field.name).toBe("primary");
|
expect(field.name).toBe("primary");
|
||||||
expect(field.schema()).toEqual(["primary", "integer", expect.any(Function)]);
|
expect(field.schema()).toEqual({
|
||||||
|
name: "primary",
|
||||||
|
type: "integer" as const,
|
||||||
|
nullable: false,
|
||||||
|
primary: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("hasDefault", async () => {
|
test("hasDefault", async () => {
|
||||||
@@ -34,4 +39,28 @@ 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,6 +1,7 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { TextField } from "../../../../src/data";
|
import { TextField, textFieldConfigSchema } from "../../../../src/data";
|
||||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
|
|
||||||
describe("[data] TextField", async () => {
|
describe("[data] TextField", async () => {
|
||||||
test("transformPersist (config)", async () => {
|
test("transformPersist (config)", async () => {
|
||||||
@@ -11,5 +12,5 @@ describe("[data] TextField", async () => {
|
|||||||
expect(transformPersist(field, "abc")).resolves.toBe("abc");
|
expect(transformPersist(field, "abc")).resolves.toBe("abc");
|
||||||
});
|
});
|
||||||
|
|
||||||
runBaseFieldTests(TextField, { defaultValue: "abc", schemaType: "text" });
|
fieldTestSuite(bunTestRunner, TextField, { defaultValue: "abc", schemaType: "text" });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ beforeAll(() =>
|
|||||||
);
|
);
|
||||||
afterAll(unmockFetch);
|
afterAll(unmockFetch);
|
||||||
|
|
||||||
describe("FetchTask", async () => {
|
describe.skip("FetchTask", async () => {
|
||||||
test("Simple fetch", async () => {
|
test("Simple fetch", async () => {
|
||||||
const task = new FetchTask("Fetch Something", {
|
const task = new FetchTask("Fetch Something", {
|
||||||
url: "https://jsonplaceholder.typicode.com/todos/1",
|
url: "https://jsonplaceholder.typicode.com/todos/1",
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Flow, LogTask, RenderTask, SubFlowTask } from "../../src/flows";
|
import { Flow, LogTask, SubFlowTask, RenderTask, Task } from "../../src/flows";
|
||||||
|
import { s } from "bknd/core";
|
||||||
|
|
||||||
describe("SubFlowTask", async () => {
|
export class StringifyTask<Output extends string> extends Task<
|
||||||
|
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",
|
||||||
@@ -22,8 +38,6 @@ describe("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");
|
||||||
});
|
});
|
||||||
@@ -40,8 +54,8 @@ describe("SubFlowTask", async () => {
|
|||||||
loop: true,
|
loop: true,
|
||||||
input: [1, 2, 3],
|
input: [1, 2, 3],
|
||||||
});
|
});
|
||||||
const task3 = new RenderTask("render2", {
|
const task3 = new StringifyTask("stringify", {
|
||||||
render: `Subflow output: {{ sub.output | join: ", " }}`,
|
input: "{{ sub.output }}",
|
||||||
});
|
});
|
||||||
|
|
||||||
const flow = new Flow("test", [task, task2, task3], []);
|
const flow = new Flow("test", [task, task2, task3], []);
|
||||||
@@ -51,41 +65,6 @@ describe("SubFlowTask", async () => {
|
|||||||
const execution = flow.createExecution();
|
const execution = flow.createExecution();
|
||||||
await execution.start();
|
await execution.start();
|
||||||
|
|
||||||
console.log("errors", execution.getErrors());
|
expect(execution.getResponse()).toEqual('"run 1,run 2,run 3"');
|
||||||
|
|
||||||
/*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 "bknd/core";
|
||||||
|
|
||||||
describe("Task", async () => {
|
describe.skip("Task", async () => {
|
||||||
test("resolveParams: template with parse", async () => {
|
test("resolveParams: template with parse", async () => {
|
||||||
const result = await Task.resolveParams(
|
const result = await Task.resolveParams(
|
||||||
Type.Object({ test: dynamic(Type.Number()) }),
|
s.object({ test: dynamic(s.number()) }),
|
||||||
{
|
{
|
||||||
test: "{{ some.path }}",
|
test: "{{ some.path }}",
|
||||||
},
|
},
|
||||||
@@ -22,7 +22,7 @@ describe("Task", async () => {
|
|||||||
|
|
||||||
test("resolveParams: with string", async () => {
|
test("resolveParams: with string", async () => {
|
||||||
const result = await Task.resolveParams(
|
const result = await Task.resolveParams(
|
||||||
Type.Object({ test: Type.String() }),
|
s.object({ test: s.string() }),
|
||||||
{
|
{
|
||||||
test: "{{ some.path }}",
|
test: "{{ some.path }}",
|
||||||
},
|
},
|
||||||
@@ -38,7 +38,7 @@ describe("Task", async () => {
|
|||||||
|
|
||||||
test("resolveParams: with object", async () => {
|
test("resolveParams: with object", async () => {
|
||||||
const result = await Task.resolveParams(
|
const result = await Task.resolveParams(
|
||||||
Type.Object({ test: dynamic(Type.Object({ key: Type.String(), value: Type.String() })) }),
|
s.object({ test: dynamic(s.object({ key: s.string(), value: s.string() })) }),
|
||||||
{
|
{
|
||||||
test: { key: "path", value: "{{ some.path }}" },
|
test: { key: "path", value: "{{ some.path }}" },
|
||||||
},
|
},
|
||||||
@@ -51,62 +51,4 @@ describe("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 { type Static, type StaticDecode, Type, parse } from "../../src/core/utils";
|
import { s, parse } from "bknd/core";
|
||||||
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 = Static<typeof OutputParamTask.schema>;
|
type OutputIn = s.Static<typeof OutputParamTask.schema>;
|
||||||
type OutputOut = StaticDecode<typeof OutputParamTask.schema>;
|
type OutputOut = s.StaticCoerced<typeof OutputParamTask.schema>;
|
||||||
|
|
||||||
class OutputParamTask extends Task<typeof OutputParamTask.schema> {
|
class OutputParamTask extends Task<typeof OutputParamTask.schema> {
|
||||||
type = "output-param";
|
type = "output-param";
|
||||||
|
|
||||||
static override schema = Type.Object({
|
static override schema = s.strictObject({
|
||||||
number: dynamic(
|
number: dynamic(
|
||||||
Type.Number({
|
s.number({
|
||||||
title: "Output number",
|
title: "Output number",
|
||||||
}),
|
}),
|
||||||
Number.parseInt,
|
Number.parseInt,
|
||||||
@@ -43,7 +43,7 @@ class PassthroughFlowInput extends Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Flow task inputs", async () => {
|
describe.skip("Flow task inputs", async () => {
|
||||||
test("types", async () => {
|
test("types", async () => {
|
||||||
const schema = OutputParamTask.schema;
|
const schema = OutputParamTask.schema;
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class ExecTask extends Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Flow trigger", async () => {
|
describe.skip("Flow trigger", async () => {
|
||||||
test("manual trigger", async () => {
|
test("manual trigger", async () => {
|
||||||
let called = false;
|
let called = false;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
// 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 { type Static, Type, _jsonp, withDisabledConsole } from "../../src/core/utils";
|
import { _jsonp, withDisabledConsole } from "../../src/core/utils";
|
||||||
|
import { s } from "bknd/core";
|
||||||
import { Condition, ExecutionEvent, FetchTask, Flow, LogTask, Task } from "../../src/flows";
|
import { Condition, ExecutionEvent, FetchTask, Flow, LogTask, Task } from "../../src/flows";
|
||||||
|
|
||||||
/*beforeAll(disableConsoleLog);
|
/*beforeAll(disableConsoleLog);
|
||||||
@@ -10,19 +11,19 @@ afterAll(enableConsoleLog);*/
|
|||||||
class ExecTask extends Task<typeof ExecTask.schema> {
|
class ExecTask extends Task<typeof ExecTask.schema> {
|
||||||
type = "exec";
|
type = "exec";
|
||||||
|
|
||||||
static override schema = Type.Object({
|
static override schema = s.object({
|
||||||
delay: Type.Number({ default: 10 }),
|
delay: s.number({ default: 10 }),
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
name: string,
|
name: string,
|
||||||
params: Static<typeof ExecTask.schema>,
|
params: s.Static<typeof ExecTask.schema>,
|
||||||
private func: () => Promise<any>,
|
private func: () => Promise<any>,
|
||||||
) {
|
) {
|
||||||
super(name, params);
|
super(name, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
override clone(name: string, params: Static<typeof ExecTask.schema>) {
|
override clone(name: string, params: s.Static<typeof ExecTask.schema>) {
|
||||||
return new ExecTask(name, params, this.func);
|
return new ExecTask(name, params, this.func);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +78,7 @@ function getObjectDiff(obj1, obj2) {
|
|||||||
return diff;
|
return diff;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Flow tests", async () => {
|
describe.skip("Flow tests", async () => {
|
||||||
test("Simple single task", async () => {
|
test("Simple single task", async () => {
|
||||||
const simple = getTask(0);
|
const simple = getTask(0);
|
||||||
|
|
||||||
|
|||||||
+50
-2
@@ -4,6 +4,9 @@ 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 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 { join } from "node:path";
|
||||||
|
import { slugify } from "core/utils/strings";
|
||||||
|
|
||||||
export function getDummyDatabase(memory: boolean = true): {
|
export function getDummyDatabase(memory: boolean = true): {
|
||||||
dummyDb: SqliteDatabase;
|
dummyDb: SqliteDatabase;
|
||||||
@@ -35,14 +38,15 @@ export function getLocalLibsqlConnection() {
|
|||||||
return { url: "http://127.0.0.1:8080" };
|
return { url: "http://127.0.0.1:8080" };
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConsoleSeverity = "log" | "warn" | "error";
|
type ConsoleSeverity = "debug" | "log" | "warn" | "error";
|
||||||
const _oldConsoles = {
|
const _oldConsoles = {
|
||||||
|
debug: console.debug,
|
||||||
log: console.log,
|
log: console.log,
|
||||||
warn: console.warn,
|
warn: console.warn,
|
||||||
error: console.error,
|
error: console.error,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function disableConsoleLog(severities: ConsoleSeverity[] = ["log", "warn"]) {
|
export function disableConsoleLog(severities: ConsoleSeverity[] = ["debug", "log", "warn"]) {
|
||||||
severities.forEach((severity) => {
|
severities.forEach((severity) => {
|
||||||
console[severity] = () => null;
|
console[severity] = () => null;
|
||||||
});
|
});
|
||||||
@@ -71,3 +75,47 @@ export function schemaToEm(s: ReturnType<typeof protoEm>, conn?: Connection): En
|
|||||||
|
|
||||||
export const assetsPath = `${import.meta.dir}/_assets`;
|
export const assetsPath = `${import.meta.dir}/_assets`;
|
||||||
export const assetsTmpPath = `${import.meta.dir}/_assets/tmp`;
|
export const assetsTmpPath = `${import.meta.dir}/_assets/tmp`;
|
||||||
|
|
||||||
|
export async function enableFetchLogging() {
|
||||||
|
const originalFetch = global.fetch;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
global.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||||
|
const response = await originalFetch(input, init);
|
||||||
|
const url = input instanceof URL || typeof input === "string" ? input : input.url;
|
||||||
|
|
||||||
|
// Only clone if it's a supported content type
|
||||||
|
const contentType = response.headers.get("content-type") || "";
|
||||||
|
const isSupported =
|
||||||
|
contentType.includes("json") ||
|
||||||
|
contentType.includes("text") ||
|
||||||
|
contentType.includes("xml");
|
||||||
|
|
||||||
|
if (isSupported) {
|
||||||
|
const clonedResponse = response.clone();
|
||||||
|
let extension = "txt";
|
||||||
|
let body: string;
|
||||||
|
|
||||||
|
if (contentType.includes("json")) {
|
||||||
|
body = JSON.stringify(await clonedResponse.json(), null, 2);
|
||||||
|
extension = "json";
|
||||||
|
} else if (contentType.includes("xml")) {
|
||||||
|
body = await clonedResponse.text();
|
||||||
|
extension = "xml";
|
||||||
|
} else {
|
||||||
|
body = await clonedResponse.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = `${new Date().getTime()}_${init?.method ?? "GET"}_${slugify(String(url))}.${extension}`;
|
||||||
|
const filePath = join(assetsTmpPath, fileName);
|
||||||
|
|
||||||
|
await writeFile(filePath, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
global.fetch = originalFetch;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 } from "../../src";
|
import { App, createApp } from "../../src";
|
||||||
import type { AuthResponse } from "../../src/auth";
|
import type { AuthResponse } from "../../src/auth";
|
||||||
import { auth } from "../../src/auth/middlewares";
|
import { auth } from "../../src/auth/middlewares";
|
||||||
import { randomString, secureRandomString, withDisabledConsole } from "../../src/core/utils";
|
import { randomString, secureRandomString, withDisabledConsole } from "../../src/core/utils";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "../helper";
|
import { disableConsoleLog, enableConsoleLog, getDummyConnection } from "../helper";
|
||||||
|
|
||||||
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
|
afterEach(afterAllCleanup);
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
beforeAll(disableConsoleLog);
|
||||||
afterAll(enableConsoleLog);
|
afterAll(enableConsoleLog);
|
||||||
@@ -64,6 +67,7 @@ const configs = {
|
|||||||
|
|
||||||
function createAuthApp() {
|
function createAuthApp() {
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
|
connection: dummyConnection,
|
||||||
initialConfig: {
|
initialConfig: {
|
||||||
auth: configs.auth,
|
auth: configs.auth,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from "bun:test";
|
import { describe, expect, it } from "bun:test";
|
||||||
import { createApp } from "../../src";
|
import { createApp } from "core/test/utils";
|
||||||
import { Api } from "../../src/Api";
|
import { Api } from "../../src/Api";
|
||||||
|
|
||||||
describe("integration config", () => {
|
describe("integration config", () => {
|
||||||
@@ -13,9 +13,8 @@ describe("integration config", () => {
|
|||||||
|
|
||||||
// create entity
|
// create entity
|
||||||
await api.system.addConfig("data", "entities.posts", {
|
await api.system.addConfig("data", "entities.posts", {
|
||||||
name: "posts",
|
|
||||||
config: { sort_field: "id", sort_dir: "asc" },
|
config: { sort_field: "id", sort_dir: "asc" },
|
||||||
fields: { id: { type: "primary", name: "id" }, asdf: { type: "text" } },
|
fields: { id: { type: "primary" }, asdf: { type: "text" } },
|
||||||
type: "regular",
|
type: "regular",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
/// <reference types="@types/bun" />
|
/// <reference types="@types/bun" />
|
||||||
|
|
||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import { createApp, registries } from "../../src";
|
import { registries } from "../../src";
|
||||||
|
import { createApp } from "core/test/utils";
|
||||||
import { mergeObject, randomString } from "../../src/core/utils";
|
import { mergeObject, randomString } from "../../src/core/utils";
|
||||||
import type { TAppMediaConfig } from "../../src/media/media-schema";
|
import type { TAppMediaConfig } from "../../src/media/media-schema";
|
||||||
import { StorageLocalAdapter } from "../../src/media/storage/adapters/StorageLocalAdapter";
|
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||||
import { assetsPath, assetsTmpPath, disableConsoleLog, enableConsoleLog } from "../helper";
|
import { assetsPath, assetsTmpPath, disableConsoleLog, enableConsoleLog } from "../helper";
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
@@ -39,11 +40,11 @@ function makeName(ext: string) {
|
|||||||
return randomString(10) + "." + ext;
|
return randomString(10) + "." + ext;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*beforeAll(disableConsoleLog);
|
beforeAll(disableConsoleLog);
|
||||||
afterAll(enableConsoleLog);*/
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
describe("MediaController", () => {
|
describe("MediaController", () => {
|
||||||
test.only("accepts direct", async () => {
|
test("accepts direct", async () => {
|
||||||
const app = await makeApp();
|
const app = await makeApp();
|
||||||
|
|
||||||
const file = Bun.file(path);
|
const file = Bun.file(path);
|
||||||
@@ -56,9 +57,9 @@ describe("MediaController", () => {
|
|||||||
console.log(result);
|
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);
|
||||||
expect(destFile.exists()).resolves.toBe(true);
|
expect(destFile.exists()).resolves.toBe(true);
|
||||||
await destFile.delete();*/
|
await destFile.delete();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("accepts form data", async () => {
|
test("accepts form data", async () => {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { type FileBody, Storage, type StorageAdapter } from "../../src/media/storage/Storage";
|
import { type FileBody, Storage } from "../../src/media/storage/Storage";
|
||||||
import * as StorageEvents from "../../src/media/storage/events";
|
import * as StorageEvents from "../../src/media/storage/events";
|
||||||
|
import { StorageAdapter } from "media";
|
||||||
|
|
||||||
class TestAdapter implements StorageAdapter {
|
class TestAdapter extends StorageAdapter {
|
||||||
files: Record<string, FileBody> = {};
|
files: Record<string, FileBody> = {};
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
@@ -61,7 +62,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", "world.txt");
|
} = await storage.uploadFile("hello" as any, "world.txt");
|
||||||
expect({ type, size }).toEqual({ type: "text/plain", size: 0 });
|
expect({ type, size }).toEqual({ type: "text/plain", size: 0 });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -71,6 +72,7 @@ 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
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
|
||||||
import { randomString } from "../../../src/core/utils";
|
|
||||||
import { StorageS3Adapter } from "../../../src/media";
|
|
||||||
|
|
||||||
import { config } from "dotenv";
|
|
||||||
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);
|
|
||||||
|
|
||||||
describe.skipIf(true)("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)).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,7 +5,9 @@ import { getRandomizedFilename } from "../../src/media/utils";
|
|||||||
|
|
||||||
describe("media/mime-types", () => {
|
describe("media/mime-types", () => {
|
||||||
test("tiny resolves", () => {
|
test("tiny resolves", () => {
|
||||||
const tests = [[".mp4", "video/mp4", ".jpg", "image/jpeg", ".zip", "application/zip"]];
|
const tests = [
|
||||||
|
[".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);
|
||||||
@@ -69,7 +71,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"],
|
||||||
];
|
] 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);
|
||||||
@@ -86,7 +88,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"],
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
for (const [filename, ext] of tests) {
|
for (const [filename, ext] of tests) {
|
||||||
expect(
|
expect(
|
||||||
@@ -94,5 +96,10 @@ 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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import { afterAll, beforeAll, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
import { afterAll, beforeAll, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
||||||
import { createApp } from "../../src";
|
import { createApp } from "core/test/utils";
|
||||||
import { AuthController } from "../../src/auth/api/AuthController";
|
import { AuthController } from "../../src/auth/api/AuthController";
|
||||||
import { em, entity, make, text } from "../../src/data";
|
import { em, entity, make, text } from "../../src/data";
|
||||||
import { AppAuth, type ModuleBuildContext } from "../../src/modules";
|
import { AppAuth, type ModuleBuildContext } from "../../src/modules";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "../helper";
|
import { disableConsoleLog, enableConsoleLog } from "../helper";
|
||||||
|
// @ts-ignore
|
||||||
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
||||||
|
|
||||||
describe("AppAuth", () => {
|
describe("AppAuth", () => {
|
||||||
|
test.only("...", () => {
|
||||||
|
const auth = new AppAuth({});
|
||||||
|
console.log(auth.toJSON());
|
||||||
|
console.log(auth.config);
|
||||||
|
});
|
||||||
|
|
||||||
moduleTestSuite(AppAuth);
|
moduleTestSuite(AppAuth);
|
||||||
|
|
||||||
let ctx: ModuleBuildContext;
|
let ctx: ModuleBuildContext;
|
||||||
@@ -22,7 +29,7 @@ describe("AppAuth", () => {
|
|||||||
|
|
||||||
const config = auth.toJSON();
|
const config = auth.toJSON();
|
||||||
expect(config.jwt).toBeUndefined();
|
expect(config.jwt).toBeUndefined();
|
||||||
expect(config.strategies.password.config).toBeUndefined();
|
expect(config.strategies?.password?.config).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("enabling auth: generate secret", async () => {
|
test("enabling auth: generate secret", async () => {
|
||||||
@@ -42,6 +49,7 @@ describe("AppAuth", () => {
|
|||||||
const auth = new AppAuth(
|
const auth = new AppAuth(
|
||||||
{
|
{
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
// @ts-ignore
|
||||||
jwt: {
|
jwt: {
|
||||||
secret: "123456",
|
secret: "123456",
|
||||||
},
|
},
|
||||||
@@ -67,7 +75,7 @@ describe("AppAuth", () => {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
email: "some@body.com",
|
email: "some@body.com",
|
||||||
password: "123456",
|
password: "12345678",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
enableConsoleLog();
|
enableConsoleLog();
|
||||||
@@ -75,7 +83,66 @@ describe("AppAuth", () => {
|
|||||||
|
|
||||||
const { data: users } = await ctx.em.repository("users").findMany();
|
const { data: users } = await ctx.em.repository("users").findMany();
|
||||||
expect(users.length).toBe(1);
|
expect(users.length).toBe(1);
|
||||||
expect(users[0].email).toBe("some@body.com");
|
expect(users[0]?.email).toBe("some@body.com");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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("$");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -92,6 +159,7 @@ 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
|
||||||
@@ -101,6 +169,10 @@ 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);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -157,7 +229,7 @@ describe("AppAuth", () => {
|
|||||||
const authField = make(name, _authFieldProto as any);
|
const authField = make(name, _authFieldProto as any);
|
||||||
const field = users.field(name)!;
|
const field = users.field(name)!;
|
||||||
for (const prop of props) {
|
for (const prop of props) {
|
||||||
expect(field.config[prop]).toBe(authField.config[prop]);
|
expect(field.config[prop]).toEqual(authField.config[prop]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,51 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { beforeEach, describe, expect, test } from "bun:test";
|
||||||
import { parse } from "../../src/core/utils";
|
import { parse } from "bknd/core";
|
||||||
import { fieldsSchema } from "../../src/data/data-schema";
|
import { fieldsSchema } from "../../src/data/data-schema";
|
||||||
import { AppData } from "../../src/modules";
|
import { AppData, type ModuleBuildContext } from "../../src/modules";
|
||||||
import { moduleTestSuite } from "./module-test-suite";
|
import { makeCtx, 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,11 +1,18 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { createApp, registries } from "../../src";
|
import { registries } from "../../src";
|
||||||
|
import { createApp } from "core/test/utils";
|
||||||
import { em, entity, text } from "../../src/data";
|
import { em, entity, text } from "../../src/data";
|
||||||
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 { mediaConfigSchema } from "../../src/media/media-schema";
|
||||||
import { moduleTestSuite } from "./module-test-suite";
|
import { moduleTestSuite } from "./module-test-suite";
|
||||||
|
|
||||||
describe("AppMedia", () => {
|
describe("AppMedia", () => {
|
||||||
|
test.only("...", () => {
|
||||||
|
const media = new AppMedia();
|
||||||
|
console.log(media.toJSON());
|
||||||
|
});
|
||||||
|
|
||||||
moduleTestSuite(AppMedia);
|
moduleTestSuite(AppMedia);
|
||||||
|
|
||||||
test("should allow additional fields", async () => {
|
test("should allow additional fields", async () => {
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { type TSchema, Type, stripMark } from "../../src/core/utils";
|
import { s, stripMark } from "bknd/core";
|
||||||
import { EntityManager, em, entity, index, text } from "../../src/data";
|
import { EntityManager, em, entity, index, text } from "../../src/data";
|
||||||
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
||||||
import { Module } from "../../src/modules/Module";
|
import { Module } from "../../src/modules/Module";
|
||||||
|
import { ModuleHelper } from "modules/ModuleHelper";
|
||||||
|
|
||||||
function createModule<Schema extends TSchema>(schema: Schema) {
|
function createModule<Schema extends s.Schema>(schema: Schema) {
|
||||||
class TestModule extends Module<typeof schema> {
|
return class TestModule extends Module<Schema> {
|
||||||
getSchema() {
|
getSchema() {
|
||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
toJSON() {
|
override toJSON() {
|
||||||
return this.config;
|
return this.config;
|
||||||
}
|
}
|
||||||
useForceParse() {
|
override useForceParse() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return TestModule;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Module", async () => {
|
describe("Module", async () => {
|
||||||
@@ -25,7 +24,7 @@ describe("Module", async () => {
|
|||||||
test("listener", async () => {
|
test("listener", async () => {
|
||||||
let result: any;
|
let result: any;
|
||||||
|
|
||||||
const module = createModule(Type.Object({ a: Type.String() }));
|
const module = createModule(s.object({ a: s.string() }));
|
||||||
const m = new module({ a: "test" });
|
const m = new module({ a: "test" });
|
||||||
|
|
||||||
await m.schema().set({ a: "test2" });
|
await m.schema().set({ a: "test2" });
|
||||||
@@ -41,13 +40,13 @@ describe("Module", async () => {
|
|||||||
describe("db schema", () => {
|
describe("db schema", () => {
|
||||||
class M extends Module {
|
class M extends Module {
|
||||||
override getSchema() {
|
override getSchema() {
|
||||||
return Type.Object({});
|
return s.object({});
|
||||||
}
|
}
|
||||||
|
|
||||||
prt = {
|
prt = {
|
||||||
ensureEntity: this.ensureEntity.bind(this),
|
ensureEntity: this.ctx.helper.ensureEntity.bind(this.ctx.helper),
|
||||||
ensureIndex: this.ensureIndex.bind(this),
|
ensureIndex: this.ctx.helper.ensureIndex.bind(this.ctx.helper),
|
||||||
ensureSchema: this.ensureSchema.bind(this),
|
ensureSchema: this.ctx.helper.ensureSchema.bind(this.ctx.helper),
|
||||||
};
|
};
|
||||||
|
|
||||||
get em() {
|
get em() {
|
||||||
@@ -62,7 +61,11 @@ describe("Module", async () => {
|
|||||||
_em.relations,
|
_em.relations,
|
||||||
_em.indices,
|
_em.indices,
|
||||||
);
|
);
|
||||||
return new M({} as any, { em, flags: Module.ctx_flags } as any);
|
const ctx = {
|
||||||
|
em,
|
||||||
|
flags: Module.ctx_flags,
|
||||||
|
};
|
||||||
|
return new M({} as any, { ...ctx, helper: new ModuleHelper(ctx as any) } as any);
|
||||||
}
|
}
|
||||||
function flat(_em: EntityManager) {
|
function flat(_em: EntityManager) {
|
||||||
return {
|
return {
|
||||||
@@ -142,14 +145,9 @@ describe("Module", async () => {
|
|||||||
|
|
||||||
// this should only add the field "important"
|
// this should only add the field "important"
|
||||||
m.prt.ensureEntity(
|
m.prt.ensureEntity(
|
||||||
entity(
|
entity("u", {
|
||||||
"u",
|
important: text(),
|
||||||
{
|
}),
|
||||||
important: text(),
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
"system",
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(m.ctx.flags.sync_required).toBe(true);
|
expect(m.ctx.flags.sync_required).toBe(true);
|
||||||
@@ -158,8 +156,7 @@ describe("Module", async () => {
|
|||||||
{
|
{
|
||||||
name: "u",
|
name: "u",
|
||||||
fields: ["id", "name", "important"],
|
fields: ["id", "name", "important"],
|
||||||
// ensured type must be present
|
type: "regular",
|
||||||
type: "system",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "p",
|
name: "p",
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
||||||
import { Type, disableConsoleLog, enableConsoleLog, stripMark } from "../../src/core/utils";
|
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||||
import { entity, text } from "../../src/data";
|
import { Connection, entity, text } from "data";
|
||||||
import { Module } from "../../src/modules/Module";
|
import { Module } from "modules/Module";
|
||||||
import { ModuleManager, getDefaultConfig } from "../../src/modules/ModuleManager";
|
import { type ConfigTable, getDefaultConfig, ModuleManager } from "modules/ModuleManager";
|
||||||
import { CURRENT_VERSION, TABLE_NAME } from "../../src/modules/migrations";
|
import { CURRENT_VERSION, TABLE_NAME } from "modules/migrations";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
|
import { s, stripMark } from "bknd/core";
|
||||||
|
|
||||||
describe("ModuleManager", async () => {
|
describe("ModuleManager", async () => {
|
||||||
test("s1: no config, no build", async () => {
|
test("s1: no config, no build", async () => {
|
||||||
@@ -43,7 +44,7 @@ describe("ModuleManager", async () => {
|
|||||||
}).toJSON(),
|
}).toJSON(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
}) as any;
|
||||||
//const { version, ...json } = mm.toJSON() as any;
|
//const { version, ...json } = mm.toJSON() as any;
|
||||||
|
|
||||||
const c2 = getDummyConnection();
|
const c2 = getDummyConnection();
|
||||||
@@ -73,7 +74,7 @@ describe("ModuleManager", async () => {
|
|||||||
}).toJSON(),
|
}).toJSON(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
} as any;
|
||||||
//const { version, ...json } = mm.toJSON() as any;
|
//const { version, ...json } = mm.toJSON() as any;
|
||||||
|
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
@@ -89,24 +90,25 @@ describe("ModuleManager", async () => {
|
|||||||
|
|
||||||
await mm2.build();
|
await mm2.build();
|
||||||
|
|
||||||
expect(stripMark(json)).toEqual(stripMark(mm2.configs()));
|
/* console.log({
|
||||||
expect(mm2.configs().data.entities.test).toBeDefined();
|
json,
|
||||||
expect(mm2.configs().data.entities.test.fields.content).toBeDefined();
|
configs: mm2.configs(),
|
||||||
expect(mm2.get("data").toJSON().entities.test.fields.content).toBeDefined();
|
}); */
|
||||||
|
//expect(stripMark(json)).toEqual(stripMark(mm2.configs()));
|
||||||
|
expect(mm2.configs().data.entities?.test).toBeDefined();
|
||||||
|
expect(mm2.configs().data.entities?.test?.fields?.content).toBeDefined();
|
||||||
|
expect(mm2.get("data").toJSON().entities?.test?.fields?.content).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("s4: config given, table exists, version outdated, migrate", async () => {
|
test("s4: config given, table exists, version outdated, migrate", async () => {
|
||||||
const c = getDummyConnection();
|
const c = getDummyConnection();
|
||||||
const mm = new ModuleManager(c.dummyConnection);
|
const mm = new ModuleManager(c.dummyConnection);
|
||||||
await mm.build();
|
await mm.build();
|
||||||
const version = mm.version();
|
|
||||||
const json = mm.configs();
|
const json = mm.configs();
|
||||||
|
|
||||||
const c2 = getDummyConnection();
|
const c2 = getDummyConnection();
|
||||||
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 },
|
|
||||||
});
|
|
||||||
await mm2.syncConfigTable();
|
await mm2.syncConfigTable();
|
||||||
|
|
||||||
await db
|
await db
|
||||||
@@ -171,7 +173,7 @@ describe("ModuleManager", async () => {
|
|||||||
expect(mm2.configs().data.basepath).toBe("/api/data2");
|
expect(mm2.configs().data.basepath).toBe("/api/data2");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("blank app, modify config", async () => {
|
/*test("blank app, modify config", async () => {
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
|
|
||||||
const mm = new ModuleManager(dummyConnection);
|
const mm = new ModuleManager(dummyConnection);
|
||||||
@@ -194,7 +196,7 @@ describe("ModuleManager", async () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});*/
|
||||||
|
|
||||||
test("partial config given", async () => {
|
test("partial config given", async () => {
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
@@ -212,13 +214,15 @@ describe("ModuleManager", async () => {
|
|||||||
expect(mm.version()).toBe(CURRENT_VERSION);
|
expect(mm.version()).toBe(CURRENT_VERSION);
|
||||||
expect(mm.built()).toBe(true);
|
expect(mm.built()).toBe(true);
|
||||||
expect(mm.configs().auth.enabled).toBe(true);
|
expect(mm.configs().auth.enabled).toBe(true);
|
||||||
expect(mm.configs().data.entities.users).toBeDefined();
|
expect(mm.configs().data.entities?.users).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("partial config given, but db version exists", async () => {
|
test("partial config given, but db version exists", async () => {
|
||||||
const c = getDummyConnection();
|
const c = getDummyConnection();
|
||||||
const mm = new ModuleManager(c.dummyConnection);
|
const mm = new ModuleManager(c.dummyConnection);
|
||||||
await mm.build();
|
await mm.build();
|
||||||
|
console.log("==".repeat(30));
|
||||||
|
console.log("");
|
||||||
const json = mm.configs();
|
const json = mm.configs();
|
||||||
|
|
||||||
const c2 = getDummyConnection();
|
const c2 = getDummyConnection();
|
||||||
@@ -255,18 +259,18 @@ describe("ModuleManager", async () => {
|
|||||||
// @todo: add tests for migrations (check "backup" and new version)
|
// @todo: add tests for migrations (check "backup" and new version)
|
||||||
|
|
||||||
describe("revert", async () => {
|
describe("revert", async () => {
|
||||||
const failingModuleSchema = Type.Object({
|
const failingModuleSchema = s.partialObject({
|
||||||
value: Type.Optional(Type.Number()),
|
value: s.number(),
|
||||||
});
|
});
|
||||||
class FailingModule extends Module<typeof failingModuleSchema> {
|
class FailingModule extends Module<s.Static<typeof failingModuleSchema>> {
|
||||||
getSchema() {
|
getSchema() {
|
||||||
return failingModuleSchema;
|
return failingModuleSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
override async build() {
|
override async build() {
|
||||||
//console.log("building FailingModule", this.config);
|
//console.log("building FailingModule", this.config);
|
||||||
if (this.config.value < 0) {
|
if (this.config.value && this.config.value < 0) {
|
||||||
throw new Error("value must be positive");
|
throw new Error("value must be positive, given: " + this.config.value);
|
||||||
}
|
}
|
||||||
this.setBuilt();
|
this.setBuilt();
|
||||||
}
|
}
|
||||||
@@ -332,6 +336,7 @@ describe("ModuleManager", async () => {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const f = mm.mutateConfigSafe("failing");
|
const f = mm.mutateConfigSafe("failing");
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
expect(f.set({ value: 2 })).resolves.toBeDefined();
|
expect(f.set({ value: 2 })).resolves.toBeDefined();
|
||||||
expect(mockOnUpdated).toHaveBeenCalled();
|
expect(mockOnUpdated).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -380,4 +385,128 @@ 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,39 +1,44 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { type InitialModuleConfigs, createApp } from "../../../src";
|
import { type InitialModuleConfigs, createApp } from "../../../src";
|
||||||
|
|
||||||
import type { Kysely } 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_2 from "./samples/v8-2.json";
|
||||||
|
|
||||||
// app expects migratable config to be present in database
|
// app expects migratable config to be present in database
|
||||||
async function createVersionedApp(config: InitialModuleConfigs) {
|
async function createVersionedApp(config: InitialModuleConfigs | any) {
|
||||||
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");
|
||||||
const { version, ...rest } = config;
|
const { version, ...rest } = config;
|
||||||
|
|
||||||
const app = createApp({ connection: dummyConnection });
|
const db = dummyConnection.kysely as Kysely<any>;
|
||||||
await app.build();
|
await sql`CREATE TABLE "__bknd" (
|
||||||
|
"id" integer not null primary key autoincrement,
|
||||||
|
"version" integer,
|
||||||
|
"type" text,
|
||||||
|
"json" text,
|
||||||
|
"created_at" datetime,
|
||||||
|
"updated_at" datetime
|
||||||
|
)`.execute(db);
|
||||||
|
|
||||||
const qb = app.modules.ctx().connection.kysely as Kysely<any>;
|
await db
|
||||||
const current = await qb
|
.insertInto("__bknd")
|
||||||
.selectFrom("__bknd")
|
.values({
|
||||||
.selectAll()
|
version,
|
||||||
.where("type", "=", "config")
|
type: "config",
|
||||||
.executeTakeFirst();
|
created_at: new Date().toISOString(),
|
||||||
|
json: JSON.stringify(rest),
|
||||||
await qb
|
})
|
||||||
.updateTable("__bknd")
|
|
||||||
.set("json", JSON.stringify(rest))
|
|
||||||
.set("version", 7)
|
|
||||||
.where("id", "=", current!.id)
|
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
const app2 = createApp({
|
const app = createApp({
|
||||||
connection: dummyConnection,
|
connection: dummyConnection,
|
||||||
});
|
});
|
||||||
await app2.build();
|
await app.build();
|
||||||
return app2;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Migrations", () => {
|
describe("Migrations", () => {
|
||||||
@@ -46,8 +51,8 @@ describe("Migrations", () => {
|
|||||||
|
|
||||||
const app = await createVersionedApp(v7);
|
const app = await createVersionedApp(v7);
|
||||||
|
|
||||||
expect(app.version()).toBe(8);
|
expect(app.version()).toBeGreaterThan(7);
|
||||||
expect(app.toJSON(true).auth.strategies.password.enabled).toBe(true);
|
expect(app.toJSON(true).auth.strategies?.password?.enabled).toBe(true);
|
||||||
|
|
||||||
const req = await app.server.request("/api/auth/password/register", {
|
const req = await app.server.request("/api/auth/password/register", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -60,7 +65,17 @@ describe("Migrations", () => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
expect(req.ok).toBe(true);
|
expect(req.ok).toBe(true);
|
||||||
const res = await req.json();
|
const res = (await req.json()) as any;
|
||||||
expect(res.user.email).toBe("test@test.com");
|
expect(res.user.email).toBe("test@test.com");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("migration from 8 to 9", async () => {
|
||||||
|
expect(v8.version).toBe(8);
|
||||||
|
|
||||||
|
const app = await createVersionedApp(v8);
|
||||||
|
|
||||||
|
expect(app.version()).toBeGreaterThan(8);
|
||||||
|
// @ts-expect-error
|
||||||
|
expect(app.toJSON(true).server.admin).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,709 @@
|
|||||||
|
{
|
||||||
|
"version": 8,
|
||||||
|
"server": {
|
||||||
|
"admin": {
|
||||||
|
"basepath": "",
|
||||||
|
"color_scheme": "light",
|
||||||
|
"logo_return_path": "/"
|
||||||
|
},
|
||||||
|
"cors": {
|
||||||
|
"origin": "*",
|
||||||
|
"allow_methods": ["GET", "POST", "PATCH", "PUT", "DELETE"],
|
||||||
|
"allow_headers": [
|
||||||
|
"Content-Type",
|
||||||
|
"Content-Length",
|
||||||
|
"Authorization",
|
||||||
|
"Accept"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"basepath": "/api/data",
|
||||||
|
"entities": {
|
||||||
|
"products": {
|
||||||
|
"type": "regular",
|
||||||
|
"fields": {
|
||||||
|
"id": {
|
||||||
|
"type": "primary",
|
||||||
|
"config": {
|
||||||
|
"fillable": false,
|
||||||
|
"required": false,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": true,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"brand": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": true,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"currency": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"price": {
|
||||||
|
"type": "number",
|
||||||
|
"config": {
|
||||||
|
"required": true,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"price_compare": {
|
||||||
|
"type": "number",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": ["table"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"html_config": {
|
||||||
|
"element": "input"
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": ["table"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "date",
|
||||||
|
"config": {
|
||||||
|
"type": "date",
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": ["table"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"html_config": {
|
||||||
|
"element": "textarea",
|
||||||
|
"props": {
|
||||||
|
"rows": 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": ["table"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"images": {
|
||||||
|
"type": "media",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": ["update"],
|
||||||
|
"hidden": false,
|
||||||
|
"mime_types": [],
|
||||||
|
"virtual": true,
|
||||||
|
"entity": "products"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"identifier": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"type": "jsonschema",
|
||||||
|
"config": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"size": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"gender": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"ai_description": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": ["string", "number"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": ["table"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_likes": {
|
||||||
|
"type": "number",
|
||||||
|
"config": {
|
||||||
|
"default_value": 0,
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"sort_field": "id",
|
||||||
|
"sort_dir": "desc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"type": "system",
|
||||||
|
"fields": {
|
||||||
|
"id": {
|
||||||
|
"type": "primary",
|
||||||
|
"config": {
|
||||||
|
"fillable": false,
|
||||||
|
"required": false,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": true,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"folder": {
|
||||||
|
"type": "boolean",
|
||||||
|
"config": {
|
||||||
|
"default_value": false,
|
||||||
|
"hidden": true,
|
||||||
|
"fillable": ["create"],
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mime_type": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"type": "number",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"hidden": true,
|
||||||
|
"fillable": ["create"],
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"etag": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modified_at": {
|
||||||
|
"type": "date",
|
||||||
|
"config": {
|
||||||
|
"type": "datetime",
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reference": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entity_id": {
|
||||||
|
"type": "number",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"type": "json",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"temporary": {
|
||||||
|
"type": "boolean",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"sort_field": "id",
|
||||||
|
"sort_dir": "desc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"type": "system",
|
||||||
|
"fields": {
|
||||||
|
"id": {
|
||||||
|
"type": "primary",
|
||||||
|
"config": {
|
||||||
|
"fillable": false,
|
||||||
|
"required": false,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": true,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"strategy": {
|
||||||
|
"type": "enum",
|
||||||
|
"config": {
|
||||||
|
"options": {
|
||||||
|
"type": "strings",
|
||||||
|
"values": ["password"]
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"fillable": ["create"],
|
||||||
|
"hidden": ["read", "table", "update", "form"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"strategy_value": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"fillable": ["create"],
|
||||||
|
"hidden": ["read", "table", "update", "form"],
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"type": "enum",
|
||||||
|
"config": {
|
||||||
|
"options": {
|
||||||
|
"type": "strings",
|
||||||
|
"values": ["guest", "admin"]
|
||||||
|
},
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_boards": {
|
||||||
|
"type": "number",
|
||||||
|
"config": {
|
||||||
|
"default_value": 0,
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"sort_field": "id",
|
||||||
|
"sort_dir": "desc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"product_likes": {
|
||||||
|
"type": "regular",
|
||||||
|
"fields": {
|
||||||
|
"id": {
|
||||||
|
"type": "primary",
|
||||||
|
"config": {
|
||||||
|
"fillable": false,
|
||||||
|
"required": false,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "date",
|
||||||
|
"config": {
|
||||||
|
"type": "datetime",
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"users_id": {
|
||||||
|
"type": "relation",
|
||||||
|
"config": {
|
||||||
|
"label": "User",
|
||||||
|
"required": true,
|
||||||
|
"reference": "users",
|
||||||
|
"target": "users",
|
||||||
|
"target_field": "id",
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false,
|
||||||
|
"on_delete": "set null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"products_id": {
|
||||||
|
"type": "relation",
|
||||||
|
"config": {
|
||||||
|
"label": "Product",
|
||||||
|
"required": true,
|
||||||
|
"reference": "products",
|
||||||
|
"target": "products",
|
||||||
|
"target_field": "id",
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false,
|
||||||
|
"on_delete": "set null"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"sort_field": "id",
|
||||||
|
"sort_dir": "desc",
|
||||||
|
"name": "Product Likes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"boards": {
|
||||||
|
"type": "regular",
|
||||||
|
"fields": {
|
||||||
|
"id": {
|
||||||
|
"type": "primary",
|
||||||
|
"config": {
|
||||||
|
"fillable": false,
|
||||||
|
"required": false,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"private": {
|
||||||
|
"type": "boolean",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"users_id": {
|
||||||
|
"type": "relation",
|
||||||
|
"config": {
|
||||||
|
"label": "Users",
|
||||||
|
"required": true,
|
||||||
|
"reference": "users",
|
||||||
|
"target": "users",
|
||||||
|
"target_field": "id",
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false,
|
||||||
|
"on_delete": "set null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"images": {
|
||||||
|
"type": "media",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": ["update"],
|
||||||
|
"hidden": false,
|
||||||
|
"mime_types": [],
|
||||||
|
"virtual": true,
|
||||||
|
"entity": "boards",
|
||||||
|
"max_items": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cover": {
|
||||||
|
"type": "number",
|
||||||
|
"config": {
|
||||||
|
"default_value": 0,
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_products": {
|
||||||
|
"type": "number",
|
||||||
|
"config": {
|
||||||
|
"default_value": 0,
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"sort_field": "id",
|
||||||
|
"sort_dir": "desc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"boards_products": {
|
||||||
|
"type": "generated",
|
||||||
|
"fields": {
|
||||||
|
"id": {
|
||||||
|
"type": "primary",
|
||||||
|
"config": {
|
||||||
|
"fillable": false,
|
||||||
|
"required": false,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"boards_id": {
|
||||||
|
"type": "relation",
|
||||||
|
"config": {
|
||||||
|
"required": true,
|
||||||
|
"reference": "boards",
|
||||||
|
"target": "boards",
|
||||||
|
"target_field": "id",
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false,
|
||||||
|
"on_delete": "set null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"products_id": {
|
||||||
|
"type": "relation",
|
||||||
|
"config": {
|
||||||
|
"required": true,
|
||||||
|
"reference": "products",
|
||||||
|
"target": "products",
|
||||||
|
"target_field": "id",
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false,
|
||||||
|
"on_delete": "set null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"manual": {
|
||||||
|
"type": "boolean",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"sort_field": "id",
|
||||||
|
"sort_dir": "desc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"relations": {
|
||||||
|
"poly_products_media_images": {
|
||||||
|
"type": "poly",
|
||||||
|
"source": "products",
|
||||||
|
"target": "media",
|
||||||
|
"config": {
|
||||||
|
"mappedBy": "images"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"n1_product_likes_users": {
|
||||||
|
"type": "n:1",
|
||||||
|
"source": "product_likes",
|
||||||
|
"target": "users",
|
||||||
|
"config": {
|
||||||
|
"mappedBy": "",
|
||||||
|
"inversedBy": "",
|
||||||
|
"required": true,
|
||||||
|
"with_limit": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"n1_product_likes_products": {
|
||||||
|
"type": "n:1",
|
||||||
|
"source": "product_likes",
|
||||||
|
"target": "products",
|
||||||
|
"config": {
|
||||||
|
"mappedBy": "",
|
||||||
|
"inversedBy": "",
|
||||||
|
"required": true,
|
||||||
|
"with_limit": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"n1_boards_users": {
|
||||||
|
"type": "n:1",
|
||||||
|
"source": "boards",
|
||||||
|
"target": "users",
|
||||||
|
"config": {
|
||||||
|
"mappedBy": "",
|
||||||
|
"inversedBy": "",
|
||||||
|
"required": true,
|
||||||
|
"with_limit": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"poly_boards_media_images": {
|
||||||
|
"type": "poly",
|
||||||
|
"source": "boards",
|
||||||
|
"target": "media",
|
||||||
|
"config": {
|
||||||
|
"mappedBy": "images",
|
||||||
|
"targetCardinality": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mn_boards_products_boards_products,boards_products": {
|
||||||
|
"type": "m:n",
|
||||||
|
"source": "boards",
|
||||||
|
"target": "products",
|
||||||
|
"config": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indices": {
|
||||||
|
"idx_unique_media_path": {
|
||||||
|
"entity": "media",
|
||||||
|
"fields": ["path"],
|
||||||
|
"unique": true
|
||||||
|
},
|
||||||
|
"idx_media_reference": {
|
||||||
|
"entity": "media",
|
||||||
|
"fields": ["reference"],
|
||||||
|
"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_product_likes_unique_products_id_users_id": {
|
||||||
|
"entity": "product_likes",
|
||||||
|
"fields": ["products_id", "users_id"],
|
||||||
|
"unique": true
|
||||||
|
},
|
||||||
|
"idx_boards_products_unique_boards_id_products_id": {
|
||||||
|
"entity": "boards_products",
|
||||||
|
"fields": ["boards_id", "products_id"],
|
||||||
|
"unique": true
|
||||||
|
},
|
||||||
|
"idx_media_entity_id": {
|
||||||
|
"entity": "media",
|
||||||
|
"fields": ["entity_id"],
|
||||||
|
"unique": false
|
||||||
|
},
|
||||||
|
"idx_products_identifier": {
|
||||||
|
"entity": "products",
|
||||||
|
"fields": ["identifier"],
|
||||||
|
"unique": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"enabled": true,
|
||||||
|
"basepath": "/api/auth",
|
||||||
|
"entity_name": "users",
|
||||||
|
"allow_register": true,
|
||||||
|
"jwt": {
|
||||||
|
"secret": "2wY76Z$JQg(3t?Wn8g,^ZqZhlmjNx<@uQjI!7i^XZBF11Xa1>zZK2??Y[D|]cc%k",
|
||||||
|
"alg": "HS256",
|
||||||
|
"fields": ["id", "email", "role"]
|
||||||
|
},
|
||||||
|
"cookie": {
|
||||||
|
"path": "/",
|
||||||
|
"sameSite": "lax",
|
||||||
|
"secure": true,
|
||||||
|
"httpOnly": true,
|
||||||
|
"expires": 604800,
|
||||||
|
"renew": true,
|
||||||
|
"pathSuccess": "/",
|
||||||
|
"pathLoggedOut": "/"
|
||||||
|
},
|
||||||
|
"strategies": {
|
||||||
|
"password": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "password",
|
||||||
|
"config": {
|
||||||
|
"hashing": "sha256"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"roles": {
|
||||||
|
"guest": {
|
||||||
|
"is_default": true,
|
||||||
|
"permissions": ["system.access.api", "data.entity.read"]
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"implicit_allow": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"guard": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"enabled": true,
|
||||||
|
"basepath": "/api/media",
|
||||||
|
"entity_name": "media",
|
||||||
|
"storage": {},
|
||||||
|
"adapter": {
|
||||||
|
"type": "s3",
|
||||||
|
"config": {
|
||||||
|
"access_key": "123",
|
||||||
|
"secret_access_key": "123",
|
||||||
|
"url": "https://123.r2.cloudflarestorage.com/enly"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flows": {
|
||||||
|
"basepath": "/api/flows",
|
||||||
|
"flows": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,598 @@
|
|||||||
|
{
|
||||||
|
"version": 8,
|
||||||
|
"server": {
|
||||||
|
"admin": {
|
||||||
|
"basepath": "",
|
||||||
|
"logo_return_path": "/",
|
||||||
|
"color_scheme": "dark"
|
||||||
|
},
|
||||||
|
"cors": {
|
||||||
|
"origin": "*",
|
||||||
|
"allow_methods": ["GET", "POST", "PATCH", "PUT", "DELETE"],
|
||||||
|
"allow_headers": [
|
||||||
|
"Content-Type",
|
||||||
|
"Content-Length",
|
||||||
|
"Authorization",
|
||||||
|
"Accept"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"basepath": "/api/data",
|
||||||
|
"entities": {
|
||||||
|
"posts": {
|
||||||
|
"type": "regular",
|
||||||
|
"fields": {
|
||||||
|
"id": {
|
||||||
|
"type": "primary",
|
||||||
|
"config": {
|
||||||
|
"fillable": false,
|
||||||
|
"required": false,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": true,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"html_config": {
|
||||||
|
"element": "input"
|
||||||
|
},
|
||||||
|
"pattern": "^[a-z\\-\\_0-9]+$",
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false,
|
||||||
|
"label": "Slug"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"html_config": {
|
||||||
|
"element": "textarea",
|
||||||
|
"props": {
|
||||||
|
"rows": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": ["form"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": {
|
||||||
|
"type": "boolean",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"images": {
|
||||||
|
"type": "media",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": ["update"],
|
||||||
|
"hidden": false,
|
||||||
|
"mime_types": [],
|
||||||
|
"virtual": true,
|
||||||
|
"entity": "posts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"type": "jsonschema",
|
||||||
|
"config": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ui_schema": {
|
||||||
|
"ui:options": {
|
||||||
|
"orderable": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"users_id": {
|
||||||
|
"type": "relation",
|
||||||
|
"config": {
|
||||||
|
"label": "Users",
|
||||||
|
"required": false,
|
||||||
|
"reference": "users",
|
||||||
|
"target": "users",
|
||||||
|
"target_field": "id",
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false,
|
||||||
|
"on_delete": "set null"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"sort_field": "title",
|
||||||
|
"sort_dir": "desc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"comments": {
|
||||||
|
"type": "regular",
|
||||||
|
"fields": {
|
||||||
|
"id": {
|
||||||
|
"type": "primary",
|
||||||
|
"config": {
|
||||||
|
"fillable": false,
|
||||||
|
"required": false,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "date",
|
||||||
|
"config": {
|
||||||
|
"type": "date",
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"posts_id": {
|
||||||
|
"type": "relation",
|
||||||
|
"config": {
|
||||||
|
"label": "Posts",
|
||||||
|
"required": true,
|
||||||
|
"reference": "posts",
|
||||||
|
"target": "posts",
|
||||||
|
"target_field": "id",
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false,
|
||||||
|
"on_delete": "set null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"users_id": {
|
||||||
|
"type": "relation",
|
||||||
|
"config": {
|
||||||
|
"label": "Users",
|
||||||
|
"required": true,
|
||||||
|
"reference": "users",
|
||||||
|
"target": "users",
|
||||||
|
"target_field": "id",
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false,
|
||||||
|
"on_delete": "set null"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"sort_field": "id",
|
||||||
|
"sort_dir": "asc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"type": "system",
|
||||||
|
"fields": {
|
||||||
|
"id": {
|
||||||
|
"type": "primary",
|
||||||
|
"config": {
|
||||||
|
"fillable": false,
|
||||||
|
"required": false,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": true,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"folder": {
|
||||||
|
"type": "boolean",
|
||||||
|
"config": {
|
||||||
|
"default_value": false,
|
||||||
|
"hidden": true,
|
||||||
|
"fillable": ["create"],
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mime_type": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"type": "number",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"hidden": true,
|
||||||
|
"fillable": ["create"],
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"etag": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modified_at": {
|
||||||
|
"type": "date",
|
||||||
|
"config": {
|
||||||
|
"type": "datetime",
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reference": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entity_id": {
|
||||||
|
"type": "number",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"type": "json",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"sort_field": "id",
|
||||||
|
"sort_dir": "asc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"type": "system",
|
||||||
|
"fields": {
|
||||||
|
"id": {
|
||||||
|
"type": "primary",
|
||||||
|
"config": {
|
||||||
|
"fillable": false,
|
||||||
|
"required": false,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": true,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"strategy": {
|
||||||
|
"type": "enum",
|
||||||
|
"config": {
|
||||||
|
"options": {
|
||||||
|
"type": "strings",
|
||||||
|
"values": ["password", "google"]
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"fillable": ["create"],
|
||||||
|
"hidden": ["update", "form"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"strategy_value": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"fillable": ["create"],
|
||||||
|
"hidden": ["read", "table", "update", "form"],
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"type": "enum",
|
||||||
|
"config": {
|
||||||
|
"options": {
|
||||||
|
"type": "strings",
|
||||||
|
"values": ["guest", "admin", "editor"]
|
||||||
|
},
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"avatar": {
|
||||||
|
"type": "media",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": ["update"],
|
||||||
|
"hidden": false,
|
||||||
|
"mime_types": [],
|
||||||
|
"virtual": true,
|
||||||
|
"entity": "users",
|
||||||
|
"max_items": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"sort_field": "id",
|
||||||
|
"sort_dir": "asc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"type": "regular",
|
||||||
|
"fields": {
|
||||||
|
"id": {
|
||||||
|
"type": "primary",
|
||||||
|
"config": {
|
||||||
|
"fillable": false,
|
||||||
|
"required": false,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"number": {
|
||||||
|
"type": "number",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"sort_field": "id",
|
||||||
|
"sort_dir": "asc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"relations": {
|
||||||
|
"n1_comments_posts": {
|
||||||
|
"type": "n:1",
|
||||||
|
"source": "comments",
|
||||||
|
"target": "posts",
|
||||||
|
"config": {
|
||||||
|
"required": true,
|
||||||
|
"with_limit": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"poly_posts_media_images": {
|
||||||
|
"type": "poly",
|
||||||
|
"source": "posts",
|
||||||
|
"target": "media",
|
||||||
|
"config": {
|
||||||
|
"mappedBy": "images"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"n1_posts_users": {
|
||||||
|
"type": "n:1",
|
||||||
|
"source": "posts",
|
||||||
|
"target": "users",
|
||||||
|
"config": {
|
||||||
|
"with_limit": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"n1_comments_users": {
|
||||||
|
"type": "n:1",
|
||||||
|
"source": "comments",
|
||||||
|
"target": "users",
|
||||||
|
"config": {
|
||||||
|
"required": true,
|
||||||
|
"with_limit": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"poly_users_media_avatar": {
|
||||||
|
"type": "poly",
|
||||||
|
"source": "users",
|
||||||
|
"target": "media",
|
||||||
|
"config": {
|
||||||
|
"mappedBy": "avatar",
|
||||||
|
"targetCardinality": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indices": {
|
||||||
|
"idx_unique_media_path": {
|
||||||
|
"entity": "media",
|
||||||
|
"fields": ["path"],
|
||||||
|
"unique": true
|
||||||
|
},
|
||||||
|
"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_media_reference": {
|
||||||
|
"entity": "media",
|
||||||
|
"fields": ["reference"],
|
||||||
|
"unique": false
|
||||||
|
},
|
||||||
|
"idx_media_entity_id": {
|
||||||
|
"entity": "media",
|
||||||
|
"fields": ["entity_id"],
|
||||||
|
"unique": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"enabled": true,
|
||||||
|
"basepath": "/api/auth",
|
||||||
|
"entity_name": "users",
|
||||||
|
"jwt": {
|
||||||
|
"secret": "A%3jk*wD!Zruj123123123j$Wm8qS8m8qS8",
|
||||||
|
"alg": "HS256",
|
||||||
|
"fields": ["id", "email", "role"],
|
||||||
|
"issuer": "showoff"
|
||||||
|
},
|
||||||
|
"guard": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"strategies": {
|
||||||
|
"password": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "password",
|
||||||
|
"config": {
|
||||||
|
"hashing": "sha256"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"google": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "oauth",
|
||||||
|
"config": {
|
||||||
|
"type": "oidc",
|
||||||
|
"client": {
|
||||||
|
"client_id": "545948917277-123ieuifrag.apps.googleusercontent.com",
|
||||||
|
"client_secret": "123-123hTTZfDDGPDPp"
|
||||||
|
},
|
||||||
|
"name": "google"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"roles": {
|
||||||
|
"guest": {
|
||||||
|
"permissions": [
|
||||||
|
"data.entity.read",
|
||||||
|
"system.access.api",
|
||||||
|
"system.config.read"
|
||||||
|
],
|
||||||
|
"is_default": true
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"is_default": false,
|
||||||
|
"implicit_allow": true
|
||||||
|
},
|
||||||
|
"editor": {
|
||||||
|
"permissions": [
|
||||||
|
"system.access.admin",
|
||||||
|
"system.config.read",
|
||||||
|
"system.schema.read",
|
||||||
|
"system.config.read.secrets",
|
||||||
|
"system.access.api",
|
||||||
|
"data.entity.read",
|
||||||
|
"data.entity.update",
|
||||||
|
"data.entity.delete",
|
||||||
|
"data.entity.create"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allow_register": true,
|
||||||
|
"cookie": {
|
||||||
|
"path": "/",
|
||||||
|
"sameSite": "lax",
|
||||||
|
"secure": true,
|
||||||
|
"httpOnly": true,
|
||||||
|
"expires": 604800,
|
||||||
|
"renew": true,
|
||||||
|
"pathSuccess": "/",
|
||||||
|
"pathLoggedOut": "/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"enabled": true,
|
||||||
|
"basepath": "/api/media",
|
||||||
|
"entity_name": "media",
|
||||||
|
"storage": {},
|
||||||
|
"adapter": {
|
||||||
|
"type": "s3",
|
||||||
|
"config": {
|
||||||
|
"access_key": "123",
|
||||||
|
"secret_access_key": "123",
|
||||||
|
"url": "https://123.r2.cloudflarestorage.com/bknd-123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flows": {
|
||||||
|
"basepath": "/api/flows",
|
||||||
|
"flows": {
|
||||||
|
"test": {
|
||||||
|
"trigger": {
|
||||||
|
"type": "http",
|
||||||
|
"config": {
|
||||||
|
"mode": "sync",
|
||||||
|
"method": "GET",
|
||||||
|
"response_type": "html",
|
||||||
|
"path": "/json-posts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tasks": {
|
||||||
|
"fetching": {
|
||||||
|
"type": "fetch",
|
||||||
|
"params": {
|
||||||
|
"method": "GET",
|
||||||
|
"headers": [],
|
||||||
|
"url": "https://jsonplaceholder.typicode.com/posts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"render": {
|
||||||
|
"type": "render",
|
||||||
|
"params": {
|
||||||
|
"render": "<h1>Posts</h1>\n<ul>\n {% for post in fetching.output %}\n <li>{{ post.title }}</li>\n {% endfor %}\n</ul>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"connections": {
|
||||||
|
"5cce66b5-57c6-4541-88ac-b298794c6c52": {
|
||||||
|
"source": "fetching",
|
||||||
|
"target": "render",
|
||||||
|
"config": {
|
||||||
|
"condition": {
|
||||||
|
"type": "success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"start_task": "fetching",
|
||||||
|
"responding_task": "render"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,14 +4,14 @@ import { Hono } from "hono";
|
|||||||
import { Guard } from "../../src/auth";
|
import { Guard } from "../../src/auth";
|
||||||
import { DebugLogger } from "../../src/core";
|
import { DebugLogger } from "../../src/core";
|
||||||
import { EventManager } from "../../src/core/events";
|
import { EventManager } from "../../src/core/events";
|
||||||
import { Default, stripMark } from "../../src/core/utils";
|
|
||||||
import { EntityManager } from "../../src/data";
|
import { EntityManager } from "../../src/data";
|
||||||
import { Module, type ModuleBuildContext } from "../../src/modules/Module";
|
import { Module, type ModuleBuildContext } from "../../src/modules/Module";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
|
import { ModuleHelper } from "modules/ModuleHelper";
|
||||||
|
|
||||||
export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildContext {
|
export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildContext {
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
return {
|
const ctx = {
|
||||||
connection: dummyConnection,
|
connection: dummyConnection,
|
||||||
server: new Hono(),
|
server: new Hono(),
|
||||||
em: new EntityManager([], dummyConnection),
|
em: new EntityManager([], dummyConnection),
|
||||||
@@ -21,6 +21,10 @@ export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildCon
|
|||||||
logger: new DebugLogger(false),
|
logger: new DebugLogger(false),
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
|
return {
|
||||||
|
...ctx,
|
||||||
|
helper: new ModuleHelper(ctx as any),
|
||||||
|
} as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function moduleTestSuite(module: { new (): Module }) {
|
export function moduleTestSuite(module: { new (): Module }) {
|
||||||
@@ -40,7 +44,8 @@ export function moduleTestSuite(module: { new (): Module }) {
|
|||||||
it("uses the default config", async () => {
|
it("uses the default config", async () => {
|
||||||
const m = new module();
|
const m = new module();
|
||||||
await m.setContext(ctx).build();
|
await m.setContext(ctx).build();
|
||||||
expect(stripMark(m.toJSON())).toEqual(Default(m.getSchema(), {}));
|
expect(m.toJSON()).toEqual(m.getSchema().template({}, { withOptional: true }));
|
||||||
|
//expect(stripMark(m.toJSON())).toEqual(Default(m.getSchema(), {}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
|
||||||
|
describe("Example Test Suite", () => {
|
||||||
|
it("should pass basic arithmetic", () => {
|
||||||
|
expect(1 + 1).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle async operations", async () => {
|
||||||
|
const result = await Promise.resolve(42);
|
||||||
|
expect(result).toBe(42);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import "@testing-library/jest-dom";
|
||||||
|
import { afterEach } from "vitest";
|
||||||
|
import { cleanup } from "@testing-library/react";
|
||||||
|
|
||||||
|
// Automatically cleanup after each test
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
//import type { BkndConfig } from "./src";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
app: {
|
|
||||||
connection: {
|
|
||||||
type: "libsql",
|
|
||||||
config: {
|
|
||||||
//url: "http://localhost:8080"
|
|
||||||
url: ":memory:"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import pkg from "./package.json" with { type: "json" };
|
||||||
|
import c from "picocolors";
|
||||||
|
import { formatNumber } from "core/utils";
|
||||||
|
|
||||||
|
const result = await Bun.build({
|
||||||
|
entrypoints: ["./src/cli/index.ts"],
|
||||||
|
target: "node",
|
||||||
|
outdir: "./dist/cli",
|
||||||
|
env: "PUBLIC_*",
|
||||||
|
minify: true,
|
||||||
|
define: {
|
||||||
|
__isDev: "0",
|
||||||
|
__version: JSON.stringify(pkg.version),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const output of result.outputs) {
|
||||||
|
const size_ = await output.text();
|
||||||
|
console.info(
|
||||||
|
c.cyan(formatNumber.fileSize(size_.length)),
|
||||||
|
c.dim(output.path.replace(import.meta.dir + "/", "")),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
import { $, type Subprocess } from "bun";
|
|
||||||
import * as esbuild from "esbuild";
|
|
||||||
import postcss from "esbuild-postcss";
|
|
||||||
import { entryOutputMeta } from "./internal/esbuild.entry-output-meta.plugin";
|
|
||||||
import { guessMimeType } from "./src/media/storage/mime-types";
|
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
|
||||||
const watch = args.includes("--watch");
|
|
||||||
const minify = args.includes("--minify");
|
|
||||||
const types = args.includes("--types");
|
|
||||||
const sourcemap = args.includes("--sourcemap");
|
|
||||||
|
|
||||||
type BuildOptions = esbuild.BuildOptions & { name: string };
|
|
||||||
|
|
||||||
const baseOptions: Partial<Omit<esbuild.BuildOptions, "plugins">> & { plugins?: any[] } = {
|
|
||||||
minify,
|
|
||||||
sourcemap,
|
|
||||||
metafile: true,
|
|
||||||
format: "esm",
|
|
||||||
drop: ["console", "debugger"],
|
|
||||||
loader: {
|
|
||||||
".svg": "dataurl",
|
|
||||||
},
|
|
||||||
define: {
|
|
||||||
__isDev: "0",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
type BuildFn = (format?: "esm" | "cjs") => BuildOptions;
|
|
||||||
|
|
||||||
// build BE
|
|
||||||
const builds: Record<string, BuildFn> = {
|
|
||||||
backend: (format = "esm") => ({
|
|
||||||
...baseOptions,
|
|
||||||
name: `backend ${format}`,
|
|
||||||
entryPoints: [
|
|
||||||
"src/index.ts",
|
|
||||||
"src/data/index.ts",
|
|
||||||
"src/core/index.ts",
|
|
||||||
"src/core/utils/index.ts",
|
|
||||||
"src/ui/index.ts",
|
|
||||||
"src/ui/main.css",
|
|
||||||
],
|
|
||||||
outdir: "dist",
|
|
||||||
outExtension: { ".js": format === "esm" ? ".js" : ".cjs" },
|
|
||||||
platform: "browser",
|
|
||||||
splitting: false,
|
|
||||||
bundle: true,
|
|
||||||
plugins: [postcss()],
|
|
||||||
//target: "es2022",
|
|
||||||
format,
|
|
||||||
}),
|
|
||||||
/*components: (format = "esm") => ({
|
|
||||||
...baseOptions,
|
|
||||||
name: `components ${format}`,
|
|
||||||
entryPoints: ["src/ui/index.ts", "src/ui/main.css"],
|
|
||||||
outdir: "dist/ui",
|
|
||||||
outExtension: { ".js": format === "esm" ? ".js" : ".cjs" },
|
|
||||||
format,
|
|
||||||
platform: "browser",
|
|
||||||
splitting: false,
|
|
||||||
//target: "es2022",
|
|
||||||
bundle: true,
|
|
||||||
//external: ["react", "react-dom", "@tanstack/react-query-devtools"],
|
|
||||||
plugins: [postcss()],
|
|
||||||
loader: {
|
|
||||||
".svg": "dataurl",
|
|
||||||
".js": "jsx"
|
|
||||||
}
|
|
||||||
}),*/
|
|
||||||
static: (format = "esm") => ({
|
|
||||||
...baseOptions,
|
|
||||||
name: `static ${format}`,
|
|
||||||
entryPoints: ["src/ui/main.tsx", "src/ui/main.css"],
|
|
||||||
entryNames: "[dir]/[name]-[hash]",
|
|
||||||
outdir: "dist/static",
|
|
||||||
outExtension: { ".js": format === "esm" ? ".js" : ".cjs" },
|
|
||||||
platform: "browser",
|
|
||||||
bundle: true,
|
|
||||||
splitting: true,
|
|
||||||
inject: ["src/ui/inject.js"],
|
|
||||||
target: "es2022",
|
|
||||||
format,
|
|
||||||
loader: {
|
|
||||||
".svg": "dataurl",
|
|
||||||
".js": "jsx",
|
|
||||||
},
|
|
||||||
define: {
|
|
||||||
__isDev: "0",
|
|
||||||
"process.env.NODE_ENV": '"production"',
|
|
||||||
},
|
|
||||||
chunkNames: "chunks/[name]-[hash]",
|
|
||||||
plugins: [
|
|
||||||
postcss(),
|
|
||||||
entryOutputMeta(async (info) => {
|
|
||||||
const manifest: Record<string, object> = {};
|
|
||||||
const toAsset = (output: string) => {
|
|
||||||
const name = output.split("/").pop()!;
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
path: output,
|
|
||||||
mime: guessMimeType(name),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
for (const { output, meta } of info) {
|
|
||||||
manifest[meta.entryPoint as string] = toAsset(output);
|
|
||||||
if (meta.cssBundle) {
|
|
||||||
manifest["src/ui/main.css"] = toAsset(meta.cssBundle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const manifest_file = "dist/static/manifest.json";
|
|
||||||
await Bun.write(manifest_file, JSON.stringify(manifest, null, 2));
|
|
||||||
console.log(`Manifest written to ${manifest_file}`, manifest);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
function adapter(adapter: string, overrides: Partial<esbuild.BuildOptions> = {}): BuildOptions {
|
|
||||||
return {
|
|
||||||
...baseOptions,
|
|
||||||
name: `adapter ${adapter} ${overrides?.format === "cjs" ? "cjs" : "esm"}`,
|
|
||||||
entryPoints: [`src/adapter/${adapter}`],
|
|
||||||
platform: "neutral",
|
|
||||||
outfile: `dist/adapter/${adapter}/index.${overrides?.format === "cjs" ? "cjs" : "js"}`,
|
|
||||||
external: [
|
|
||||||
"cloudflare:workers",
|
|
||||||
"@hono*",
|
|
||||||
"hono*",
|
|
||||||
"bknd*",
|
|
||||||
"*.html",
|
|
||||||
"node*",
|
|
||||||
"react*",
|
|
||||||
"next*",
|
|
||||||
"libsql",
|
|
||||||
"@libsql*",
|
|
||||||
],
|
|
||||||
splitting: false,
|
|
||||||
treeShaking: true,
|
|
||||||
bundle: true,
|
|
||||||
...overrides,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const adapters = [
|
|
||||||
adapter("vite", { platform: "node" }),
|
|
||||||
adapter("cloudflare"),
|
|
||||||
adapter("nextjs", { platform: "node", format: "esm" }),
|
|
||||||
adapter("nextjs", { platform: "node", format: "cjs" }),
|
|
||||||
adapter("remix", { format: "esm" }),
|
|
||||||
adapter("remix", { format: "cjs" }),
|
|
||||||
adapter("bun"),
|
|
||||||
adapter("node", { platform: "node", format: "esm" }),
|
|
||||||
adapter("node", { platform: "node", format: "cjs" }),
|
|
||||||
];
|
|
||||||
|
|
||||||
const collect = [
|
|
||||||
builds.static(),
|
|
||||||
builds.backend(),
|
|
||||||
//builds.components(),
|
|
||||||
builds.backend("cjs"),
|
|
||||||
//builds.components("cjs"),
|
|
||||||
...adapters,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (watch) {
|
|
||||||
const _state: {
|
|
||||||
timeout: Timer | undefined;
|
|
||||||
cleanup: Subprocess | undefined;
|
|
||||||
building: Subprocess | undefined;
|
|
||||||
} = {
|
|
||||||
timeout: undefined,
|
|
||||||
cleanup: undefined,
|
|
||||||
building: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
async function rebuildTypes() {
|
|
||||||
if (!types) return;
|
|
||||||
if (_state.timeout) {
|
|
||||||
clearTimeout(_state.timeout);
|
|
||||||
if (_state.cleanup) _state.cleanup.kill();
|
|
||||||
if (_state.building) _state.building.kill();
|
|
||||||
}
|
|
||||||
_state.timeout = setTimeout(async () => {
|
|
||||||
_state.cleanup = Bun.spawn(["bun", "clean:types"], {
|
|
||||||
onExit: () => {
|
|
||||||
_state.cleanup = undefined;
|
|
||||||
_state.building = Bun.spawn(["bun", "build:types"], {
|
|
||||||
onExit: () => {
|
|
||||||
_state.building = undefined;
|
|
||||||
console.log("Types rebuilt");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const { name, ...build } of collect) {
|
|
||||||
const ctx = await esbuild.context({
|
|
||||||
...build,
|
|
||||||
plugins: [
|
|
||||||
...(build.plugins ?? []),
|
|
||||||
{
|
|
||||||
name: "rebuild-notify",
|
|
||||||
setup(build) {
|
|
||||||
build.onEnd((result) => {
|
|
||||||
console.log(`rebuilt ${name} with ${result.errors.length} errors`);
|
|
||||||
rebuildTypes();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
ctx.watch();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await $`rm -rf dist`;
|
|
||||||
|
|
||||||
async function _build() {
|
|
||||||
let i = 0;
|
|
||||||
const count = collect.length;
|
|
||||||
for await (const { name, ...build } of collect) {
|
|
||||||
await esbuild.build({
|
|
||||||
...build,
|
|
||||||
plugins: [
|
|
||||||
...(build.plugins || []),
|
|
||||||
{
|
|
||||||
name: "progress",
|
|
||||||
setup(build) {
|
|
||||||
i++;
|
|
||||||
build.onEnd((result) => {
|
|
||||||
const errors = result.errors.length;
|
|
||||||
const from = String(i).padStart(String(count).length);
|
|
||||||
console.log(`[${from}/${count}] built ${name} with ${errors} errors`);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("All builds complete");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function _buildtypes() {
|
|
||||||
if (!types) return;
|
|
||||||
Bun.spawn(["bun", "build:types"], {
|
|
||||||
onExit: () => {
|
|
||||||
console.log("Types rebuilt");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all([_build(), _buildtypes()]);
|
|
||||||
}
|
|
||||||
+76
-14
@@ -1,5 +1,6 @@
|
|||||||
import { $ } from "bun";
|
import { $ } from "bun";
|
||||||
import * as tsup from "tsup";
|
import * as tsup from "tsup";
|
||||||
|
import pkg from "./package.json" with { type: "json" };
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
const watch = args.includes("--watch");
|
const watch = args.includes("--watch");
|
||||||
@@ -8,8 +9,13 @@ const types = args.includes("--types");
|
|||||||
const sourcemap = args.includes("--sourcemap");
|
const sourcemap = args.includes("--sourcemap");
|
||||||
const clean = args.includes("--clean");
|
const clean = args.includes("--clean");
|
||||||
|
|
||||||
|
const define = {
|
||||||
|
__isDev: "0",
|
||||||
|
__version: JSON.stringify(pkg.version),
|
||||||
|
};
|
||||||
|
|
||||||
if (clean) {
|
if (clean) {
|
||||||
console.log("Cleaning dist (w/o static)");
|
console.info("Cleaning dist (w/o static)");
|
||||||
await $`find dist -mindepth 1 ! -path "dist/static/*" ! -path "dist/static" -exec rm -rf {} +`;
|
await $`find dist -mindepth 1 ! -path "dist/static/*" ! -path "dist/static" -exec rm -rf {} +`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,11 +27,11 @@ function buildTypes() {
|
|||||||
Bun.spawn(["bun", "build:types"], {
|
Bun.spawn(["bun", "build:types"], {
|
||||||
stdout: "inherit",
|
stdout: "inherit",
|
||||||
onExit: () => {
|
onExit: () => {
|
||||||
console.log("Types built");
|
console.info("Types built");
|
||||||
Bun.spawn(["bun", "tsc-alias"], {
|
Bun.spawn(["bun", "tsc-alias"], {
|
||||||
stdout: "inherit",
|
stdout: "inherit",
|
||||||
onExit: () => {
|
onExit: () => {
|
||||||
console.log("Types aliased");
|
console.info("Types aliased");
|
||||||
types_running = false;
|
types_running = false;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -46,17 +52,43 @@ if (types && !watch) {
|
|||||||
buildTypes();
|
buildTypes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function banner(title: string) {
|
||||||
|
console.info("");
|
||||||
|
console.info("=".repeat(40));
|
||||||
|
console.info(title.toUpperCase());
|
||||||
|
console.info("-".repeat(40));
|
||||||
|
}
|
||||||
|
|
||||||
|
// collection of always-external packages
|
||||||
|
const external = [
|
||||||
|
"bun:test",
|
||||||
|
"node:test",
|
||||||
|
"node:assert/strict",
|
||||||
|
"@libsql/client",
|
||||||
|
"bknd",
|
||||||
|
/^bknd\/.*/,
|
||||||
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Building backend and general API
|
* Building backend and general API
|
||||||
*/
|
*/
|
||||||
async function buildApi() {
|
async function buildApi() {
|
||||||
|
banner("Building API");
|
||||||
await tsup.build({
|
await tsup.build({
|
||||||
minify,
|
minify,
|
||||||
sourcemap,
|
sourcemap,
|
||||||
watch,
|
watch,
|
||||||
entry: ["src/index.ts", "src/data/index.ts", "src/core/index.ts", "src/core/utils/index.ts"],
|
define,
|
||||||
|
entry: [
|
||||||
|
"src/index.ts",
|
||||||
|
"src/core/index.ts",
|
||||||
|
"src/core/utils/index.ts",
|
||||||
|
"src/data/index.ts",
|
||||||
|
"src/media/index.ts",
|
||||||
|
"src/plugins/index.ts",
|
||||||
|
],
|
||||||
outDir: "dist",
|
outDir: "dist",
|
||||||
external: ["bun:test", "@libsql/client"],
|
external: [...external],
|
||||||
metafile: true,
|
metafile: true,
|
||||||
platform: "browser",
|
platform: "browser",
|
||||||
format: ["esm"],
|
format: ["esm"],
|
||||||
@@ -84,8 +116,9 @@ async function buildUi() {
|
|||||||
minify,
|
minify,
|
||||||
sourcemap,
|
sourcemap,
|
||||||
watch,
|
watch,
|
||||||
|
define,
|
||||||
external: [
|
external: [
|
||||||
"bun:test",
|
...external,
|
||||||
"react",
|
"react",
|
||||||
"react-dom",
|
"react-dom",
|
||||||
"react/jsx-runtime",
|
"react/jsx-runtime",
|
||||||
@@ -109,6 +142,7 @@ async function buildUi() {
|
|||||||
},
|
},
|
||||||
} satisfies tsup.Options;
|
} satisfies tsup.Options;
|
||||||
|
|
||||||
|
banner("Building UI");
|
||||||
await tsup.build({
|
await tsup.build({
|
||||||
...base,
|
...base,
|
||||||
entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"],
|
entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"],
|
||||||
@@ -119,6 +153,7 @@ async function buildUi() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
banner("Building Client");
|
||||||
await tsup.build({
|
await tsup.build({
|
||||||
...base,
|
...base,
|
||||||
entry: ["src/ui/client/index.ts"],
|
entry: ["src/ui/client/index.ts"],
|
||||||
@@ -136,10 +171,12 @@ async function buildUi() {
|
|||||||
* - ui/client is external, and after built replaced with "bknd/client"
|
* - ui/client is external, and after built replaced with "bknd/client"
|
||||||
*/
|
*/
|
||||||
async function buildUiElements() {
|
async function buildUiElements() {
|
||||||
|
banner("Building UI Elements");
|
||||||
await tsup.build({
|
await tsup.build({
|
||||||
minify,
|
minify,
|
||||||
sourcemap,
|
sourcemap,
|
||||||
watch,
|
watch,
|
||||||
|
define,
|
||||||
entry: ["src/ui/elements/index.ts"],
|
entry: ["src/ui/elements/index.ts"],
|
||||||
outDir: "dist/ui/elements",
|
outDir: "dist/ui/elements",
|
||||||
external: [
|
external: [
|
||||||
@@ -191,20 +228,22 @@ function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsu
|
|||||||
},
|
},
|
||||||
...overrides,
|
...overrides,
|
||||||
define: {
|
define: {
|
||||||
__isDev: "0",
|
...define,
|
||||||
...overrides.define,
|
...overrides.define,
|
||||||
},
|
},
|
||||||
external: [
|
external: [
|
||||||
/^cloudflare*/,
|
/^cloudflare*/,
|
||||||
/^@?(hono|libsql).*?/,
|
/^@?hono.*?/,
|
||||||
/^(bknd|react|next|node).*?/,
|
/^(bknd|react|next|node).*?/,
|
||||||
/.*\.(html)$/,
|
/.*\.(html)$/,
|
||||||
|
...external,
|
||||||
...(Array.isArray(overrides.external) ? overrides.external : []),
|
...(Array.isArray(overrides.external) ? overrides.external : []),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildAdapters() {
|
async function buildAdapters() {
|
||||||
|
banner("Building Adapters");
|
||||||
// base adapter handles
|
// base adapter handles
|
||||||
await tsup.build({
|
await tsup.build({
|
||||||
...baseConfig(""),
|
...baseConfig(""),
|
||||||
@@ -213,15 +252,15 @@ async function buildAdapters() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// specific adatpers
|
// specific adatpers
|
||||||
await tsup.build(baseConfig("remix"));
|
await tsup.build(baseConfig("react-router"));
|
||||||
await tsup.build(baseConfig("bun"));
|
|
||||||
await tsup.build(baseConfig("astro"));
|
|
||||||
await tsup.build(baseConfig("aws"));
|
|
||||||
await tsup.build(
|
await tsup.build(
|
||||||
baseConfig("cloudflare", {
|
baseConfig("bun", {
|
||||||
external: [/^kysely/],
|
external: [/^bun\:.*/],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
await tsup.build(baseConfig("astro"));
|
||||||
|
await tsup.build(baseConfig("aws"));
|
||||||
|
await tsup.build(baseConfig("cloudflare"));
|
||||||
|
|
||||||
await tsup.build({
|
await tsup.build({
|
||||||
...baseConfig("vite"),
|
...baseConfig("vite"),
|
||||||
@@ -237,6 +276,29 @@ async function buildAdapters() {
|
|||||||
...baseConfig("node"),
|
...baseConfig("node"),
|
||||||
platform: "node",
|
platform: "node",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await tsup.build({
|
||||||
|
...baseConfig("sqlite/edge"),
|
||||||
|
entry: ["src/adapter/sqlite/edge.ts"],
|
||||||
|
outDir: "dist/adapter/sqlite",
|
||||||
|
metafile: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await tsup.build({
|
||||||
|
...baseConfig("sqlite/node"),
|
||||||
|
entry: ["src/adapter/sqlite/node.ts"],
|
||||||
|
outDir: "dist/adapter/sqlite",
|
||||||
|
platform: "node",
|
||||||
|
metafile: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await tsup.build({
|
||||||
|
...baseConfig("sqlite/bun"),
|
||||||
|
entry: ["src/adapter/sqlite/bun.ts"],
|
||||||
|
outDir: "dist/adapter/sqlite",
|
||||||
|
metafile: false,
|
||||||
|
external: [/^bun\:.*/],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await buildApi();
|
await buildApi();
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
import { $ } from "bun";
|
||||||
|
import path from "node:path";
|
||||||
|
import c from "picocolors";
|
||||||
|
|
||||||
|
const basePath = new URL(import.meta.resolve("../../")).pathname.slice(0, -1);
|
||||||
|
|
||||||
|
async function run(
|
||||||
|
cmd: string[] | string,
|
||||||
|
opts: Bun.SpawnOptions.OptionsObject & {},
|
||||||
|
onChunk: (chunk: string, resolve: (data: any) => void, reject: (err: Error) => void) => void,
|
||||||
|
): Promise<{ proc: Bun.Subprocess; data: any }> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const proc = Bun.spawn(Array.isArray(cmd) ? cmd : cmd.split(" "), {
|
||||||
|
...opts,
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Read from stdout
|
||||||
|
const reader = proc.stdout.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
|
// Function to read chunks
|
||||||
|
let resolveCalled = false;
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
const text = decoder.decode(value);
|
||||||
|
if (!resolveCalled) {
|
||||||
|
console.log(c.dim(text.replace(/\n$/, "")));
|
||||||
|
}
|
||||||
|
onChunk(
|
||||||
|
text,
|
||||||
|
(data) => {
|
||||||
|
resolve({ proc, data });
|
||||||
|
resolveCalled = true;
|
||||||
|
},
|
||||||
|
reject,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
proc.exited.then((code) => {
|
||||||
|
if (code !== 0 && code !== 130) {
|
||||||
|
throw new Error(`Process exited with code ${code}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const adapters = {
|
||||||
|
node: {
|
||||||
|
dir: path.join(basePath, "examples/node"),
|
||||||
|
clean: async function () {
|
||||||
|
const cwd = path.relative(process.cwd(), this.dir);
|
||||||
|
await $`cd ${cwd} && rm -rf uploads data.db && mkdir -p uploads`;
|
||||||
|
},
|
||||||
|
start: async function () {
|
||||||
|
return await run(
|
||||||
|
"npm run start",
|
||||||
|
{
|
||||||
|
cwd: this.dir,
|
||||||
|
},
|
||||||
|
(chunk, resolve, reject) => {
|
||||||
|
const regex = /running on (http:\/\/.*)\n/;
|
||||||
|
if (regex.test(chunk)) {
|
||||||
|
resolve(chunk.match(regex)?.[1]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bun: {
|
||||||
|
dir: path.join(basePath, "examples/bun"),
|
||||||
|
clean: async function () {
|
||||||
|
const cwd = path.relative(process.cwd(), this.dir);
|
||||||
|
await $`cd ${cwd} && rm -rf uploads data.db && mkdir -p uploads`;
|
||||||
|
},
|
||||||
|
start: async function () {
|
||||||
|
return await run(
|
||||||
|
"npm run start",
|
||||||
|
{
|
||||||
|
cwd: this.dir,
|
||||||
|
},
|
||||||
|
(chunk, resolve, reject) => {
|
||||||
|
const regex = /running on (http:\/\/.*)\n/;
|
||||||
|
if (regex.test(chunk)) {
|
||||||
|
resolve(chunk.match(regex)?.[1]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cloudflare: {
|
||||||
|
dir: path.join(basePath, "examples/cloudflare-worker"),
|
||||||
|
clean: async function () {
|
||||||
|
const cwd = path.relative(process.cwd(), this.dir);
|
||||||
|
await $`cd ${cwd} && rm -rf .wrangler node_modules/.cache node_modules/.mf`;
|
||||||
|
},
|
||||||
|
start: async function () {
|
||||||
|
return await run(
|
||||||
|
"npm run dev",
|
||||||
|
{
|
||||||
|
cwd: this.dir,
|
||||||
|
},
|
||||||
|
(chunk, resolve, reject) => {
|
||||||
|
const regex = /Ready on (http:\/\/.*)/;
|
||||||
|
if (regex.test(chunk)) {
|
||||||
|
resolve(chunk.match(regex)?.[1]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"react-router": {
|
||||||
|
dir: path.join(basePath, "examples/react-router"),
|
||||||
|
clean: async function () {
|
||||||
|
const cwd = path.relative(process.cwd(), this.dir);
|
||||||
|
await $`cd ${cwd} && rm -rf .react-router data.db`;
|
||||||
|
await $`cd ${cwd} && rm -rf public/uploads && mkdir -p public/uploads`;
|
||||||
|
},
|
||||||
|
start: async function () {
|
||||||
|
return await run(
|
||||||
|
"npm run dev",
|
||||||
|
{
|
||||||
|
cwd: this.dir,
|
||||||
|
},
|
||||||
|
(chunk, resolve, reject) => {
|
||||||
|
const regex = /Local.*?(http:\/\/.*)\//;
|
||||||
|
if (regex.test(chunk)) {
|
||||||
|
resolve(chunk.match(regex)?.[1]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nextjs: {
|
||||||
|
dir: path.join(basePath, "examples/nextjs"),
|
||||||
|
env: "TEST_TIMEOUT=20000",
|
||||||
|
clean: async function () {
|
||||||
|
const cwd = path.relative(process.cwd(), this.dir);
|
||||||
|
await $`cd ${cwd} && rm -rf .nextjs data.db`;
|
||||||
|
await $`cd ${cwd} && rm -rf public/uploads && mkdir -p public/uploads`;
|
||||||
|
},
|
||||||
|
start: async function () {
|
||||||
|
return await run(
|
||||||
|
"npm run dev",
|
||||||
|
{
|
||||||
|
cwd: this.dir,
|
||||||
|
},
|
||||||
|
(chunk, resolve, reject) => {
|
||||||
|
const regex = /Local.*?(http:\/\/.*)\n/;
|
||||||
|
if (regex.test(chunk)) {
|
||||||
|
resolve(chunk.match(regex)?.[1]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
astro: {
|
||||||
|
dir: path.join(basePath, "examples/astro"),
|
||||||
|
clean: async function () {
|
||||||
|
const cwd = path.relative(process.cwd(), this.dir);
|
||||||
|
await $`cd ${cwd} && rm -rf .astro data.db`;
|
||||||
|
await $`cd ${cwd} && rm -rf public/uploads && mkdir -p public/uploads`;
|
||||||
|
},
|
||||||
|
start: async function () {
|
||||||
|
return await run(
|
||||||
|
"npm run dev",
|
||||||
|
{
|
||||||
|
cwd: this.dir,
|
||||||
|
},
|
||||||
|
(chunk, resolve, reject) => {
|
||||||
|
const regex = /Local.*?(http:\/\/.*)\//;
|
||||||
|
if (regex.test(chunk)) {
|
||||||
|
resolve(chunk.match(regex)?.[1]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
async function testAdapter(name: keyof typeof adapters) {
|
||||||
|
const config = adapters[name];
|
||||||
|
console.log("adapter", c.cyan(name));
|
||||||
|
await config.clean();
|
||||||
|
|
||||||
|
const { proc, data } = await config.start();
|
||||||
|
console.log("proc:", proc.pid, "data:", c.cyan(data));
|
||||||
|
//proc.kill();process.exit(0);
|
||||||
|
|
||||||
|
const add_env = "env" in config && config.env ? config.env : "";
|
||||||
|
await $`TEST_URL=${data} TEST_ADAPTER=${name} ${add_env} bun run test:e2e`;
|
||||||
|
console.log("DONE!");
|
||||||
|
|
||||||
|
while (!proc.killed) {
|
||||||
|
proc.kill("SIGINT");
|
||||||
|
await Bun.sleep(250);
|
||||||
|
console.log("Waiting for process to exit...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// run with: TEST_ADAPTER=astro bun run e2e/adapters.ts
|
||||||
|
// (modify `test:e2e` to `test:e2e:ui` to see the UI)
|
||||||
|
if (process.env.TEST_ADAPTER) {
|
||||||
|
await testAdapter(process.env.TEST_ADAPTER as any);
|
||||||
|
} else {
|
||||||
|
for (const [name] of Object.entries(adapters)) {
|
||||||
|
await testAdapter(name as any);
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 265 KiB |
@@ -0,0 +1,25 @@
|
|||||||
|
// @ts-check
|
||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
import { testIds } from "../src/ui/lib/config";
|
||||||
|
|
||||||
|
import { getAdapterConfig } from "./inc/adapters";
|
||||||
|
const config = getAdapterConfig();
|
||||||
|
|
||||||
|
test("start page has expected title", async ({ page }) => {
|
||||||
|
await page.goto(config.base_path);
|
||||||
|
await expect(page).toHaveTitle(/BKND/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("start page has expected heading", async ({ page }) => {
|
||||||
|
await page.goto(config.base_path);
|
||||||
|
|
||||||
|
// Example of checking if a heading with "No entity selected" exists and is visible
|
||||||
|
const heading = page.getByRole("heading", { name: /No entity selected/i });
|
||||||
|
await expect(heading).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("modal opens on button click", async ({ page }) => {
|
||||||
|
await page.goto(config.base_path);
|
||||||
|
await page.getByTestId(testIds.data.btnCreateEntity).click();
|
||||||
|
await expect(page.getByRole("dialog")).toBeVisible();
|
||||||
|
});
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
const adapter = process.env.TEST_ADAPTER;
|
||||||
|
|
||||||
|
const default_config = {
|
||||||
|
media_adapter: "local",
|
||||||
|
base_path: "",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const configs = {
|
||||||
|
cloudflare: {
|
||||||
|
media_adapter: "r2",
|
||||||
|
},
|
||||||
|
"react-router": {
|
||||||
|
base_path: "/admin",
|
||||||
|
},
|
||||||
|
nextjs: {
|
||||||
|
base_path: "/admin",
|
||||||
|
},
|
||||||
|
astro: {
|
||||||
|
base_path: "/admin",
|
||||||
|
},
|
||||||
|
node: {
|
||||||
|
base_path: "",
|
||||||
|
},
|
||||||
|
bun: {
|
||||||
|
base_path: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getAdapterConfig(): typeof default_config {
|
||||||
|
if (adapter) {
|
||||||
|
if (!configs[adapter]) {
|
||||||
|
console.warn(
|
||||||
|
`Adapter "${adapter}" not found. Available adapters: ${Object.keys(configs).join(", ")}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
...default_config,
|
||||||
|
...configs[adapter],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return default_config;
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// @ts-check
|
||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
import { testIds } from "../src/ui/lib/config";
|
||||||
|
import type { SchemaResponse } from "../src/modules/server/SystemController";
|
||||||
|
import { getAdapterConfig } from "./inc/adapters";
|
||||||
|
|
||||||
|
// Annotate entire file as serial.
|
||||||
|
test.describe.configure({ mode: "serial" });
|
||||||
|
|
||||||
|
const config = getAdapterConfig();
|
||||||
|
|
||||||
|
test("can enable media", async ({ page }) => {
|
||||||
|
await page.goto(`${config.base_path}/media/settings`);
|
||||||
|
|
||||||
|
// enable
|
||||||
|
const enableToggle = page.getByTestId(testIds.media.switchEnabled);
|
||||||
|
if ((await enableToggle.getAttribute("aria-checked")) !== "true") {
|
||||||
|
await expect(enableToggle).toBeVisible();
|
||||||
|
await enableToggle.click();
|
||||||
|
await expect(enableToggle).toHaveAttribute("aria-checked", "true");
|
||||||
|
|
||||||
|
// select local
|
||||||
|
const adapterChoice = page.locator(`css=button#adapter-${config.media_adapter}`);
|
||||||
|
await expect(adapterChoice).toBeVisible();
|
||||||
|
await adapterChoice.click();
|
||||||
|
|
||||||
|
// save
|
||||||
|
const saveBtn = page.getByRole("button", { name: /Update/i });
|
||||||
|
await expect(saveBtn).toBeVisible();
|
||||||
|
|
||||||
|
// intercept network request, wait for it to finish and get the response
|
||||||
|
const [request] = await Promise.all([
|
||||||
|
page.waitForRequest((request) => request.url().includes("api/system/schema")),
|
||||||
|
saveBtn.click(),
|
||||||
|
]);
|
||||||
|
const response = await request.response();
|
||||||
|
expect(response?.status(), "fresh config 200").toBe(200);
|
||||||
|
const body = (await response?.json()) as SchemaResponse;
|
||||||
|
expect(body.config.media.enabled, "media is enabled").toBe(true);
|
||||||
|
expect(body.config.media.adapter?.type, "correct adapter").toBe(config.media_adapter);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can upload a file", async ({ page }) => {
|
||||||
|
await page.goto(`${config.base_path}/media`);
|
||||||
|
// check any text to contain "Upload files"
|
||||||
|
await expect(page.getByText(/Upload files/i)).toBeVisible();
|
||||||
|
|
||||||
|
// upload a file from disk
|
||||||
|
// Start waiting for file chooser before clicking. Note no await.
|
||||||
|
const fileChooserPromise = page.waitForEvent("filechooser");
|
||||||
|
await page.getByText("Upload file").click();
|
||||||
|
const fileChooser = await fileChooserPromise;
|
||||||
|
await fileChooser.setFiles("./e2e/assets/image.jpg");
|
||||||
|
});
|
||||||
+147
-75
@@ -3,8 +3,8 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"bin": "./dist/cli/index.js",
|
"bin": "./dist/cli/index.js",
|
||||||
"version": "0.9.0",
|
"version": "0.16.0-rc.0",
|
||||||
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
|
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
|
||||||
"homepage": "https://bknd.io",
|
"homepage": "https://bknd.io",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -13,13 +13,15 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/bknd-io/bknd/issues"
|
"url": "https://github.com/bknd-io/bknd/issues"
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "BKND_CLI_LOG_LEVEL=debug vite",
|
||||||
"test": "ALL_TESTS=1 bun test --bail",
|
|
||||||
"test:coverage": "ALL_TESTS=1 bun test --bail --coverage",
|
|
||||||
"build": "NODE_ENV=production bun run build.ts --minify --types",
|
"build": "NODE_ENV=production bun run build.ts --minify --types",
|
||||||
"build:all": "rm -rf dist && bun run build:static && NODE_ENV=production bun run build.ts --minify --types --clean && bun run build:cli",
|
"build:all": "rm -rf dist && bun run build:static && NODE_ENV=production bun run build.ts --minify --types --clean && bun run build:cli",
|
||||||
"build:cli": "bun build src/cli/index.ts --target node --outdir dist/cli --minify",
|
"build:ci": "mkdir -p dist/static/.vite && echo '{}' > dist/static/.vite/manifest.json && NODE_ENV=production bun run build.ts",
|
||||||
|
"build:cli": "bun run build.cli.ts",
|
||||||
"build:static": "vite build",
|
"build:static": "vite build",
|
||||||
"watch": "bun run build.ts --types --watch",
|
"watch": "bun run build.ts --types --watch",
|
||||||
"types": "bun tsc -p tsconfig.build.json --noEmit",
|
"types": "bun tsc -p tsconfig.build.json --noEmit",
|
||||||
@@ -27,86 +29,113 @@
|
|||||||
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly && tsc-alias",
|
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly && tsc-alias",
|
||||||
"updater": "bun x npm-check-updates -ui",
|
"updater": "bun x npm-check-updates -ui",
|
||||||
"cli": "LOCAL=1 bun src/cli/index.ts",
|
"cli": "LOCAL=1 bun src/cli/index.ts",
|
||||||
"prepublishOnly": "bun run types && bun run test && bun run build:all && cp ../README.md ./",
|
"prepublishOnly": "bun run types && bun run test && bun run test:node && bun run test:e2e && bun run build:all && cp ../README.md ./",
|
||||||
"postpublish": "rm -f README.md"
|
"postpublish": "rm -f README.md",
|
||||||
|
"test": "ALL_TESTS=1 bun test --bail",
|
||||||
|
"test:all": "bun run test && bun run test:node",
|
||||||
|
"test:bun": "ALL_TESTS=1 bun test --bail",
|
||||||
|
"test:node": "vitest run",
|
||||||
|
"test:adapters": "bun test src/adapter/**/*.adapter.spec.ts --bail",
|
||||||
|
"test:coverage": "ALL_TESTS=1 bun test --bail --coverage",
|
||||||
|
"test:vitest:coverage": "vitest run --coverage",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:adapters": "bun run e2e/adapters.ts",
|
||||||
|
"test:e2e:ui": "playwright test --ui",
|
||||||
|
"test:e2e:debug": "playwright test --debug",
|
||||||
|
"test:e2e:report": "playwright show-report"
|
||||||
},
|
},
|
||||||
"license": "FSL-1.1-MIT",
|
"license": "FSL-1.1-MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cfworker/json-schema": "^2.0.1",
|
"@cfworker/json-schema": "^4.1.1",
|
||||||
"@codemirror/lang-html": "^6.4.9",
|
"@codemirror/lang-html": "^6.4.9",
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@codemirror/lang-liquid": "^6.2.1",
|
"@hello-pangea/dnd": "^18.0.1",
|
||||||
"@hello-pangea/dnd": "^17.0.0",
|
"@hono/swagger-ui": "^0.5.1",
|
||||||
"@libsql/client": "^0.14.0",
|
"@mantine/core": "^7.17.1",
|
||||||
"@mantine/core": "^7.13.4",
|
"@mantine/hooks": "^7.17.1",
|
||||||
"@sinclair/typebox": "^0.32.34",
|
"@tanstack/react-form": "^1.0.5",
|
||||||
"@tanstack/react-form": "0.19.2",
|
"@uiw/react-codemirror": "^4.23.10",
|
||||||
"@uiw/react-codemirror": "^4.23.6",
|
"@xyflow/react": "^12.4.4",
|
||||||
"@xyflow/react": "^12.3.2",
|
"aws4fetch": "^1.0.20",
|
||||||
"aws4fetch": "^1.0.18",
|
"bcryptjs": "^3.0.2",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"fast-xml-parser": "^4.4.0",
|
"fast-xml-parser": "^5.0.8",
|
||||||
"hono": "^4.6.12",
|
"hono": "^4.7.11",
|
||||||
"json-schema-form-react": "^0.0.2",
|
"json-schema-form-react": "^0.0.2",
|
||||||
"json-schema-library": "^10.0.0-rc7",
|
"json-schema-library": "10.0.0-rc7",
|
||||||
"json-schema-to-ts": "^3.1.1",
|
"json-schema-to-ts": "^3.1.1",
|
||||||
"kysely": "^0.27.4",
|
"kysely": "^0.27.6",
|
||||||
"liquidjs": "^10.15.0",
|
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"oauth4webapi": "^2.11.1",
|
"oauth4webapi": "^2.11.1",
|
||||||
"object-path-immutable": "^4.1.2",
|
"object-path-immutable": "^4.1.2",
|
||||||
"picocolors": "^1.1.1",
|
"radix-ui": "^1.1.3",
|
||||||
"radix-ui": "^1.1.2",
|
"swr": "^2.3.3"
|
||||||
"swr": "^2.2.5"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.613.0",
|
"@aws-sdk/client-s3": "^3.758.0",
|
||||||
"@bluwy/giget-core": "^0.1.2",
|
"@bluwy/giget-core": "^0.1.2",
|
||||||
|
"@cloudflare/vitest-pool-workers": "^0.8.38",
|
||||||
|
"@cloudflare/workers-types": "^4.20250606.0",
|
||||||
"@dagrejs/dagre": "^1.1.4",
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
"@mantine/modals": "^7.13.4",
|
"@hono/vite-dev-server": "^0.19.1",
|
||||||
"@mantine/notifications": "^7.13.4",
|
"@hookform/resolvers": "^4.1.3",
|
||||||
"@hono/typebox-validator": "^0.2.6",
|
"@libsql/client": "^0.15.9",
|
||||||
"@hono/vite-dev-server": "^0.17.0",
|
"@mantine/modals": "^7.17.1",
|
||||||
"@hono/zod-validator": "^0.4.1",
|
"@mantine/notifications": "^7.17.1",
|
||||||
"@hookform/resolvers": "^3.9.1",
|
"@playwright/test": "^1.51.1",
|
||||||
"@libsql/kysely-libsql": "^0.4.1",
|
|
||||||
"@rjsf/core": "5.22.2",
|
"@rjsf/core": "5.22.2",
|
||||||
|
"@standard-schema/spec": "^1.0.0",
|
||||||
"@tabler/icons-react": "3.18.0",
|
"@tabler/icons-react": "3.18.0",
|
||||||
"@types/node": "^22.10.0",
|
"@tailwindcss/postcss": "^4.0.12",
|
||||||
"@types/react": "^18.3.12",
|
"@tailwindcss/vite": "^4.0.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@vitejs/plugin-react": "^4.3.3",
|
"@testing-library/react": "^16.2.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"@types/node": "^22.13.10",
|
||||||
|
"@types/react": "^19.0.10",
|
||||||
|
"@types/react-dom": "^19.0.4",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"@vitest/coverage-v8": "^3.0.9",
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"esbuild-postcss": "^0.0.4",
|
"jotai": "^2.12.2",
|
||||||
"jotai": "^2.10.1",
|
"jsdom": "^26.0.0",
|
||||||
|
"jsonv-ts": "^0.2.2",
|
||||||
"kysely-d1": "^0.3.0",
|
"kysely-d1": "^0.3.0",
|
||||||
|
"kysely-generic-sqlite": "^1.2.1",
|
||||||
|
"libsql-stateless-easy": "^1.8.0",
|
||||||
"open": "^10.1.0",
|
"open": "^10.1.0",
|
||||||
"openapi-types": "^12.1.3",
|
"openapi-types": "^12.1.3",
|
||||||
"postcss": "^8.4.47",
|
"picocolors": "^1.1.1",
|
||||||
|
"postcss": "^8.5.3",
|
||||||
"postcss-preset-mantine": "^1.17.0",
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"react-hook-form": "^7.53.1",
|
"posthog-js-lite": "^3.4.2",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-hook-form": "^7.54.2",
|
||||||
"react-icons": "5.2.1",
|
"react-icons": "5.2.1",
|
||||||
"react-json-view-lite": "^2.0.1",
|
"react-json-view-lite": "^2.4.1",
|
||||||
"sql-formatter": "^15.4.9",
|
"sql-formatter": "^15.4.11",
|
||||||
"tailwind-merge": "^2.5.4",
|
"tailwind-merge": "^3.0.2",
|
||||||
"tailwindcss": "^3.4.14",
|
"tailwindcss": "^4.0.12",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tsc-alias": "^1.8.10",
|
"tsc-alias": "^1.8.11",
|
||||||
"tsup": "^8.3.5",
|
"tsup": "^8.4.0",
|
||||||
"vite": "^5.4.10",
|
"tsx": "^4.19.3",
|
||||||
"vite-plugin-static-copy": "^2.0.0",
|
"uuid": "^11.1.0",
|
||||||
"vite-tsconfig-paths": "^5.0.1",
|
"vite": "^6.3.5",
|
||||||
"wouter": "^3.3.5"
|
"vite-plugin-circular-dependency": "^0.5.0",
|
||||||
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
|
"vitest": "^3.0.9",
|
||||||
|
"wouter": "^3.6.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@hono/node-server": "^1.13.7"
|
"@hono/node-server": "^1.14.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": ">=18",
|
"react": ">=19",
|
||||||
"react-dom": ">=18"
|
"react-dom": ">=19"
|
||||||
},
|
},
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"module": "./dist/index.js",
|
"module": "./dist/index.js",
|
||||||
@@ -115,47 +144,71 @@
|
|||||||
".": {
|
".": {
|
||||||
"types": "./dist/types/index.d.ts",
|
"types": "./dist/types/index.d.ts",
|
||||||
"import": "./dist/index.js",
|
"import": "./dist/index.js",
|
||||||
"require": "./dist/index.cjs"
|
"require": "./dist/index.js"
|
||||||
},
|
},
|
||||||
"./ui": {
|
"./ui": {
|
||||||
"types": "./dist/types/ui/index.d.ts",
|
"types": "./dist/types/ui/index.d.ts",
|
||||||
"import": "./dist/ui/index.js",
|
"import": "./dist/ui/index.js",
|
||||||
"require": "./dist/ui/index.cjs"
|
"require": "./dist/ui/index.js"
|
||||||
},
|
},
|
||||||
"./elements": {
|
"./elements": {
|
||||||
"types": "./dist/types/ui/elements/index.d.ts",
|
"types": "./dist/types/ui/elements/index.d.ts",
|
||||||
"import": "./dist/ui/elements/index.js",
|
"import": "./dist/ui/elements/index.js",
|
||||||
"require": "./dist/ui/elements/index.cjs"
|
"require": "./dist/ui/elements/index.js"
|
||||||
},
|
},
|
||||||
"./client": {
|
"./client": {
|
||||||
"types": "./dist/types/ui/client/index.d.ts",
|
"types": "./dist/types/ui/client/index.d.ts",
|
||||||
"import": "./dist/ui/client/index.js",
|
"import": "./dist/ui/client/index.js",
|
||||||
"require": "./dist/ui/client/index.cjs"
|
"require": "./dist/ui/client/index.js"
|
||||||
},
|
},
|
||||||
"./data": {
|
"./data": {
|
||||||
"types": "./dist/types/data/index.d.ts",
|
"types": "./dist/types/data/index.d.ts",
|
||||||
"import": "./dist/data/index.js",
|
"import": "./dist/data/index.js",
|
||||||
"require": "./dist/data/index.cjs"
|
"require": "./dist/data/index.js"
|
||||||
},
|
},
|
||||||
"./core": {
|
"./core": {
|
||||||
"types": "./dist/types/core/index.d.ts",
|
"types": "./dist/types/core/index.d.ts",
|
||||||
"import": "./dist/core/index.js",
|
"import": "./dist/core/index.js",
|
||||||
"require": "./dist/core/index.cjs"
|
"require": "./dist/core/index.js"
|
||||||
},
|
},
|
||||||
"./utils": {
|
"./utils": {
|
||||||
"types": "./dist/types/core/utils/index.d.ts",
|
"types": "./dist/types/core/utils/index.d.ts",
|
||||||
"import": "./dist/core/utils/index.js",
|
"import": "./dist/core/utils/index.js",
|
||||||
"require": "./dist/core/utils/index.cjs"
|
"require": "./dist/core/utils/index.js"
|
||||||
},
|
},
|
||||||
"./cli": {
|
"./cli": {
|
||||||
"types": "./dist/types/cli/index.d.ts",
|
"types": "./dist/types/cli/index.d.ts",
|
||||||
"import": "./dist/cli/index.js",
|
"import": "./dist/cli/index.js",
|
||||||
"require": "./dist/cli/index.cjs"
|
"require": "./dist/cli/index.js"
|
||||||
|
},
|
||||||
|
"./media": {
|
||||||
|
"types": "./dist/types/media/index.d.ts",
|
||||||
|
"import": "./dist/media/index.js",
|
||||||
|
"require": "./dist/media/index.js"
|
||||||
|
},
|
||||||
|
"./plugins": {
|
||||||
|
"types": "./dist/types/plugins/index.d.ts",
|
||||||
|
"import": "./dist/plugins/index.js",
|
||||||
|
"require": "./dist/plugins/index.js"
|
||||||
|
},
|
||||||
|
"./adapter/sqlite": {
|
||||||
|
"types": "./dist/types/adapter/sqlite/edge.d.ts",
|
||||||
|
"import": {
|
||||||
|
"workerd": "./dist/adapter/sqlite/edge.js",
|
||||||
|
"edge-light": "./dist/adapter/sqlite/edge.js",
|
||||||
|
"netlify": "./dist/adapter/sqlite/edge.js",
|
||||||
|
"vercel": "./dist/adapter/sqlite/edge.js",
|
||||||
|
"browser": "./dist/adapter/sqlite/edge.js",
|
||||||
|
"bun": "./dist/adapter/sqlite/bun.js",
|
||||||
|
"node": "./dist/adapter/sqlite/node.js",
|
||||||
|
"default": "./dist/adapter/sqlite/node.js"
|
||||||
|
},
|
||||||
|
"require": "./dist/adapter/sqlite/node.js"
|
||||||
},
|
},
|
||||||
"./adapter/cloudflare": {
|
"./adapter/cloudflare": {
|
||||||
"types": "./dist/types/adapter/cloudflare/index.d.ts",
|
"types": "./dist/types/adapter/cloudflare/index.d.ts",
|
||||||
"import": "./dist/adapter/cloudflare/index.js",
|
"import": "./dist/adapter/cloudflare/index.js",
|
||||||
"require": "./dist/adapter/cloudflare/index.cjs"
|
"require": "./dist/adapter/cloudflare/index.js"
|
||||||
},
|
},
|
||||||
"./adapter": {
|
"./adapter": {
|
||||||
"types": "./dist/types/adapter/index.d.ts",
|
"types": "./dist/types/adapter/index.d.ts",
|
||||||
@@ -164,42 +217,60 @@
|
|||||||
"./adapter/vite": {
|
"./adapter/vite": {
|
||||||
"types": "./dist/types/adapter/vite/index.d.ts",
|
"types": "./dist/types/adapter/vite/index.d.ts",
|
||||||
"import": "./dist/adapter/vite/index.js",
|
"import": "./dist/adapter/vite/index.js",
|
||||||
"require": "./dist/adapter/vite/index.cjs"
|
"require": "./dist/adapter/vite/index.js"
|
||||||
},
|
},
|
||||||
"./adapter/nextjs": {
|
"./adapter/nextjs": {
|
||||||
"types": "./dist/types/adapter/nextjs/index.d.ts",
|
"types": "./dist/types/adapter/nextjs/index.d.ts",
|
||||||
"import": "./dist/adapter/nextjs/index.js",
|
"import": "./dist/adapter/nextjs/index.js",
|
||||||
"require": "./dist/adapter/nextjs/index.cjs"
|
"require": "./dist/adapter/nextjs/index.js"
|
||||||
},
|
},
|
||||||
"./adapter/remix": {
|
"./adapter/react-router": {
|
||||||
"types": "./dist/types/adapter/remix/index.d.ts",
|
"types": "./dist/types/adapter/react-router/index.d.ts",
|
||||||
"import": "./dist/adapter/remix/index.js",
|
"import": "./dist/adapter/react-router/index.js",
|
||||||
"require": "./dist/adapter/remix/index.cjs"
|
"require": "./dist/adapter/react-router/index.js"
|
||||||
},
|
},
|
||||||
"./adapter/bun": {
|
"./adapter/bun": {
|
||||||
"types": "./dist/types/adapter/bun/index.d.ts",
|
"types": "./dist/types/adapter/bun/index.d.ts",
|
||||||
"import": "./dist/adapter/bun/index.js",
|
"import": "./dist/adapter/bun/index.js",
|
||||||
"require": "./dist/adapter/bun/index.cjs"
|
"require": "./dist/adapter/bun/index.js"
|
||||||
},
|
},
|
||||||
"./adapter/node": {
|
"./adapter/node": {
|
||||||
"types": "./dist/types/adapter/node/index.d.ts",
|
"types": "./dist/types/adapter/node/index.d.ts",
|
||||||
"import": "./dist/adapter/node/index.js",
|
"import": "./dist/adapter/node/index.js",
|
||||||
"require": "./dist/adapter/node/index.cjs"
|
"require": "./dist/adapter/node/index.js"
|
||||||
},
|
},
|
||||||
"./adapter/astro": {
|
"./adapter/astro": {
|
||||||
"types": "./dist/types/adapter/astro/index.d.ts",
|
"types": "./dist/types/adapter/astro/index.d.ts",
|
||||||
"import": "./dist/adapter/astro/index.js",
|
"import": "./dist/adapter/astro/index.js",
|
||||||
"require": "./dist/adapter/astro/index.cjs"
|
"require": "./dist/adapter/astro/index.js"
|
||||||
},
|
},
|
||||||
"./adapter/aws": {
|
"./adapter/aws": {
|
||||||
"types": "./dist/types/adapter/aws/index.d.ts",
|
"types": "./dist/types/adapter/aws/index.d.ts",
|
||||||
"import": "./dist/adapter/aws/index.js",
|
"import": "./dist/adapter/aws/index.js",
|
||||||
"require": "./dist/adapter/aws/index.cjs"
|
"require": "./dist/adapter/aws/index.js"
|
||||||
},
|
},
|
||||||
"./dist/main.css": "./dist/ui/main.css",
|
"./dist/main.css": "./dist/ui/main.css",
|
||||||
"./dist/styles.css": "./dist/ui/styles.css",
|
"./dist/styles.css": "./dist/ui/styles.css",
|
||||||
"./dist/manifest.json": "./dist/static/.vite/manifest.json"
|
"./dist/manifest.json": "./dist/static/.vite/manifest.json"
|
||||||
},
|
},
|
||||||
|
"typesVersions": {
|
||||||
|
"*": {
|
||||||
|
"data": ["./dist/types/data/index.d.ts"],
|
||||||
|
"core": ["./dist/types/core/index.d.ts"],
|
||||||
|
"utils": ["./dist/types/core/utils/index.d.ts"],
|
||||||
|
"cli": ["./dist/types/cli/index.d.ts"],
|
||||||
|
"media": ["./dist/types/media/index.d.ts"],
|
||||||
|
"plugins": ["./dist/types/plugins/index.d.ts"],
|
||||||
|
"adapter": ["./dist/types/adapter/index.d.ts"],
|
||||||
|
"adapter/cloudflare": ["./dist/types/adapter/cloudflare/index.d.ts"],
|
||||||
|
"adapter/vite": ["./dist/types/adapter/vite/index.d.ts"],
|
||||||
|
"adapter/nextjs": ["./dist/types/adapter/nextjs/index.d.ts"],
|
||||||
|
"adapter/react-router": ["./dist/types/adapter/react-router/index.d.ts"],
|
||||||
|
"adapter/bun": ["./dist/types/adapter/bun/index.d.ts"],
|
||||||
|
"adapter/node": ["./dist/types/adapter/node/index.d.ts"],
|
||||||
|
"adapter/sqlite": ["./dist/types/adapter/sqlite/edge.d.ts"]
|
||||||
|
}
|
||||||
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
@@ -224,6 +295,7 @@
|
|||||||
"cloudflare",
|
"cloudflare",
|
||||||
"nextjs",
|
"nextjs",
|
||||||
"remix",
|
"remix",
|
||||||
|
"react-router",
|
||||||
"astro",
|
"astro",
|
||||||
"bun",
|
"bun",
|
||||||
"node"
|
"node"
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
|
const baseUrl = process.env.TEST_URL || "http://localhost:28623";
|
||||||
|
const startCommand = process.env.TEST_START_COMMAND || "bun run dev";
|
||||||
|
const autoStart = ["1", "true", undefined].includes(process.env.TEST_AUTO_START);
|
||||||
|
const timeout = process.env.TEST_TIMEOUT ? Number.parseInt(process.env.TEST_TIMEOUT) : 5000;
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testMatch: "**/*.e2e-spec.ts",
|
||||||
|
testDir: "./e2e",
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
reporter: "html",
|
||||||
|
timeout,
|
||||||
|
use: {
|
||||||
|
baseURL: baseUrl,
|
||||||
|
trace: "on-first-retry",
|
||||||
|
video: "on-first-retry",
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "chromium",
|
||||||
|
use: { ...devices["Desktop Chrome"] },
|
||||||
|
},
|
||||||
|
/* {
|
||||||
|
name: "firefox",
|
||||||
|
use: { ...devices["Desktop Firefox"] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "webkit",
|
||||||
|
use: { ...devices["Desktop Safari"] },
|
||||||
|
}, */
|
||||||
|
],
|
||||||
|
webServer: autoStart
|
||||||
|
? {
|
||||||
|
command: startCommand,
|
||||||
|
url: baseUrl,
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
"postcss-import": {},
|
"@tailwindcss/postcss": {},
|
||||||
"tailwindcss/nesting": {},
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
"postcss-preset-mantine": {},
|
"postcss-preset-mantine": {},
|
||||||
"postcss-simple-vars": {
|
"postcss-simple-vars": {
|
||||||
variables: {
|
variables: {
|
||||||
|
|||||||
+82
-25
@@ -1,13 +1,19 @@
|
|||||||
import type { SafeUser } from "auth";
|
import type { SafeUser } from "auth";
|
||||||
import { AuthApi } from "auth/api/AuthApi";
|
import { AuthApi, type AuthApiOptions } from "auth/api/AuthApi";
|
||||||
import { DataApi } from "data/api/DataApi";
|
import { DataApi, type DataApiOptions } from "data/api/DataApi";
|
||||||
import { decode } from "hono/jwt";
|
import { decode } from "hono/jwt";
|
||||||
import { MediaApi } from "media/api/MediaApi";
|
import { MediaApi, type MediaApiOptions } from "media/api/MediaApi";
|
||||||
import { SystemApi } from "modules/SystemApi";
|
import { SystemApi } from "modules/SystemApi";
|
||||||
import { omitKeys } from "core/utils";
|
import { omitKeys } from "core/utils";
|
||||||
|
import type { BaseModuleApiOptions } from "modules";
|
||||||
|
|
||||||
export type TApiUser = SafeUser;
|
export type TApiUser = SafeUser;
|
||||||
|
|
||||||
|
export type ApiFetcher = (
|
||||||
|
input: RequestInfo | URL,
|
||||||
|
init?: RequestInit,
|
||||||
|
) => Response | Promise<Response>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
__BKND__: {
|
__BKND__: {
|
||||||
@@ -16,14 +22,24 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SubApiOptions<T extends BaseModuleApiOptions> = Omit<T, keyof BaseModuleApiOptions>;
|
||||||
|
|
||||||
export type ApiOptions = {
|
export type ApiOptions = {
|
||||||
host?: string;
|
host?: string;
|
||||||
headers?: Headers;
|
headers?: Headers;
|
||||||
key?: string;
|
key?: string;
|
||||||
localStorage?: boolean;
|
storage?: {
|
||||||
fetcher?: typeof fetch;
|
getItem: (key: string) => string | undefined | null | Promise<string | undefined | null>;
|
||||||
|
setItem: (key: string, value: string) => void | Promise<void>;
|
||||||
|
removeItem: (key: string) => void | Promise<void>;
|
||||||
|
};
|
||||||
|
onAuthStateChange?: (state: AuthState) => void;
|
||||||
|
fetcher?: ApiFetcher;
|
||||||
verbose?: boolean;
|
verbose?: boolean;
|
||||||
verified?: boolean;
|
verified?: boolean;
|
||||||
|
data?: SubApiOptions<DataApiOptions>;
|
||||||
|
auth?: SubApiOptions<AuthApiOptions>;
|
||||||
|
media?: SubApiOptions<MediaApiOptions>;
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
token?: string;
|
token?: string;
|
||||||
@@ -56,18 +72,18 @@ export class Api {
|
|||||||
this.verified = options.verified === true;
|
this.verified = options.verified === true;
|
||||||
|
|
||||||
// prefer request if given
|
// prefer request if given
|
||||||
if ("request" in options) {
|
if ("request" in options && options.request) {
|
||||||
this.options.host = options.host ?? new URL(options.request.url).origin;
|
this.options.host = options.host ?? new URL(options.request.url).origin;
|
||||||
this.options.headers = options.headers ?? options.request.headers;
|
this.options.headers = options.headers ?? options.request.headers;
|
||||||
this.extractToken();
|
this.extractToken();
|
||||||
|
|
||||||
// then check for a token
|
// then check for a token
|
||||||
} else if ("token" in options) {
|
} else if ("token" in options && options.token) {
|
||||||
this.token_transport = "header";
|
this.token_transport = "header";
|
||||||
this.updateToken(options.token);
|
this.updateToken(options.token, { trigger: false });
|
||||||
|
|
||||||
// then check for an user object
|
// then check for an user object
|
||||||
} else if ("user" in options) {
|
} else if ("user" in options && options.user) {
|
||||||
this.token_transport = "none";
|
this.token_transport = "none";
|
||||||
this.user = options.user;
|
this.user = options.user;
|
||||||
this.verified = options.verified !== false;
|
this.verified = options.verified !== false;
|
||||||
@@ -78,6 +94,10 @@ export class Api {
|
|||||||
this.buildApis();
|
this.buildApis();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get fetcher() {
|
||||||
|
return this.options.fetcher ?? fetch;
|
||||||
|
}
|
||||||
|
|
||||||
get baseUrl() {
|
get baseUrl() {
|
||||||
return this.options.host ?? "http://localhost";
|
return this.options.host ?? "http://localhost";
|
||||||
}
|
}
|
||||||
@@ -106,18 +126,30 @@ export class Api {
|
|||||||
this.updateToken(headerToken);
|
this.updateToken(headerToken);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (this.options.localStorage) {
|
} else if (this.storage) {
|
||||||
const token = localStorage.getItem(this.tokenKey);
|
this.storage.getItem(this.tokenKey).then((token) => {
|
||||||
if (token) {
|
|
||||||
this.token_transport = "header";
|
this.token_transport = "header";
|
||||||
this.updateToken(token);
|
this.updateToken(token ? String(token) : undefined);
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//console.warn("Couldn't extract token");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateToken(token?: string, rebuild?: boolean) {
|
private get storage() {
|
||||||
|
if (!this.options.storage) return null;
|
||||||
|
return {
|
||||||
|
getItem: async (key: string) => {
|
||||||
|
return await this.options.storage!.getItem(key);
|
||||||
|
},
|
||||||
|
setItem: async (key: string, value: string) => {
|
||||||
|
return await this.options.storage!.setItem(key, value);
|
||||||
|
},
|
||||||
|
removeItem: async (key: string) => {
|
||||||
|
return await this.options.storage!.removeItem(key);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
updateToken(token?: string, opts?: { rebuild?: boolean; trigger?: boolean }) {
|
||||||
this.token = token;
|
this.token = token;
|
||||||
this.verified = false;
|
this.verified = false;
|
||||||
|
|
||||||
@@ -127,17 +159,25 @@ export class Api {
|
|||||||
this.user = undefined;
|
this.user = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.localStorage) {
|
if (this.storage) {
|
||||||
const key = this.tokenKey;
|
const key = this.tokenKey;
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
localStorage.setItem(key, token);
|
this.storage.setItem(key, token).then(() => {
|
||||||
|
this.options.onAuthStateChange?.(this.getAuthState());
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem(key);
|
this.storage.removeItem(key).then(() => {
|
||||||
|
this.options.onAuthStateChange?.(this.getAuthState());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (opts?.trigger !== false) {
|
||||||
|
this.options.onAuthStateChange?.(this.getAuthState());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rebuild) this.buildApis();
|
if (opts?.rebuild) this.buildApis();
|
||||||
}
|
}
|
||||||
|
|
||||||
private markAuthVerified(verfied: boolean) {
|
private markAuthVerified(verfied: boolean) {
|
||||||
@@ -207,15 +247,32 @@ export class Api {
|
|||||||
const fetcher = this.options.fetcher;
|
const fetcher = this.options.fetcher;
|
||||||
|
|
||||||
this.system = new SystemApi(baseParams, fetcher);
|
this.system = new SystemApi(baseParams, fetcher);
|
||||||
this.data = new DataApi(baseParams, fetcher);
|
this.data = new DataApi(
|
||||||
this.auth = new AuthApi(
|
|
||||||
{
|
{
|
||||||
...baseParams,
|
...baseParams,
|
||||||
onTokenUpdate: (token) => this.updateToken(token, true),
|
...this.options.data,
|
||||||
|
},
|
||||||
|
fetcher,
|
||||||
|
);
|
||||||
|
this.auth = new AuthApi(
|
||||||
|
{
|
||||||
|
...baseParams,
|
||||||
|
credentials: this.options.storage ? "omit" : "include",
|
||||||
|
...this.options.auth,
|
||||||
|
onTokenUpdate: (token) => {
|
||||||
|
this.updateToken(token, { rebuild: true });
|
||||||
|
this.options.auth?.onTokenUpdate?.(token);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fetcher,
|
||||||
|
);
|
||||||
|
this.media = new MediaApi(
|
||||||
|
{
|
||||||
|
...baseParams,
|
||||||
|
...this.options.media,
|
||||||
},
|
},
|
||||||
fetcher,
|
fetcher,
|
||||||
);
|
);
|
||||||
this.media = new MediaApi(baseParams, fetcher);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+172
-82
@@ -1,26 +1,43 @@
|
|||||||
import type { CreateUserPayload } from "auth/AppAuth";
|
import type { CreateUserPayload } from "auth/AppAuth";
|
||||||
import { $console } from "core";
|
import { $console } from "core/utils";
|
||||||
import { Event } from "core/events";
|
import { Event } from "core/events";
|
||||||
import { Connection, type LibSqlCredentials, LibsqlConnection } from "data";
|
import type { em as prototypeEm } from "data/prototype";
|
||||||
|
import { Connection } from "data/connection/Connection";
|
||||||
import type { Hono } from "hono";
|
import type { Hono } from "hono";
|
||||||
import {
|
import {
|
||||||
|
ModuleManager,
|
||||||
type InitialModuleConfigs,
|
type InitialModuleConfigs,
|
||||||
type ModuleBuildContext,
|
type ModuleBuildContext,
|
||||||
ModuleManager,
|
type ModuleConfigs,
|
||||||
type ModuleManagerOptions,
|
type ModuleManagerOptions,
|
||||||
type Modules,
|
type Modules,
|
||||||
} from "modules/ModuleManager";
|
} from "modules/ModuleManager";
|
||||||
import * as SystemPermissions from "modules/permissions";
|
import * as SystemPermissions from "modules/permissions";
|
||||||
import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
|
import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
|
||||||
import { SystemController } from "modules/server/SystemController";
|
import { SystemController } from "modules/server/SystemController";
|
||||||
|
import type { MaybePromise } from "core/types";
|
||||||
|
import type { ServerEnv } from "modules/Controller";
|
||||||
|
import type { IEmailDriver, ICacheDriver } from "core/drivers";
|
||||||
|
|
||||||
// biome-ignore format: must be there
|
// biome-ignore format: must be here
|
||||||
import { Api, type ApiOptions } from "Api";
|
import { Api, type ApiOptions } from "Api";
|
||||||
|
|
||||||
export type AppPlugin = (app: App) => Promise<void> | void;
|
export type AppPluginConfig = {
|
||||||
|
name: string;
|
||||||
|
schema?: () => MaybePromise<ReturnType<typeof prototypeEm> | void>;
|
||||||
|
beforeBuild?: () => MaybePromise<void>;
|
||||||
|
onBuilt?: () => MaybePromise<void>;
|
||||||
|
onServerInit?: (server: Hono<ServerEnv>) => MaybePromise<void>;
|
||||||
|
onFirstBoot?: () => MaybePromise<void>;
|
||||||
|
onBoot?: () => MaybePromise<void>;
|
||||||
|
};
|
||||||
|
export type AppPlugin = (app: App) => AppPluginConfig;
|
||||||
|
|
||||||
abstract class AppEvent<A = {}> extends Event<{ app: App } & A> {}
|
abstract class AppEvent<A = {}> extends Event<{ app: App } & A> {}
|
||||||
export class AppConfigUpdatedEvent extends AppEvent {
|
export class AppConfigUpdatedEvent extends AppEvent<{
|
||||||
|
module: string;
|
||||||
|
config: ModuleConfigs[keyof ModuleConfigs];
|
||||||
|
}> {
|
||||||
static override slug = "app-config-updated";
|
static override slug = "app-config-updated";
|
||||||
}
|
}
|
||||||
export class AppBuiltEvent extends AppEvent {
|
export class AppBuiltEvent extends AppEvent {
|
||||||
@@ -29,22 +46,32 @@ export class AppBuiltEvent extends AppEvent {
|
|||||||
export class AppFirstBoot extends AppEvent {
|
export class AppFirstBoot extends AppEvent {
|
||||||
static override slug = "app-first-boot";
|
static override slug = "app-first-boot";
|
||||||
}
|
}
|
||||||
export const AppEvents = { AppConfigUpdatedEvent, AppBuiltEvent, AppFirstBoot } as const;
|
export class AppRequest extends AppEvent<{ request: Request }> {
|
||||||
|
static override slug = "app-request";
|
||||||
|
}
|
||||||
|
export class AppBeforeResponse extends AppEvent<{ request: Request; response: Response }> {
|
||||||
|
static override slug = "app-before-response";
|
||||||
|
}
|
||||||
|
export const AppEvents = {
|
||||||
|
AppConfigUpdatedEvent,
|
||||||
|
AppBuiltEvent,
|
||||||
|
AppFirstBoot,
|
||||||
|
AppRequest,
|
||||||
|
AppBeforeResponse,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export type AppOptions = {
|
export type AppOptions = {
|
||||||
plugins?: AppPlugin[];
|
plugins?: AppPlugin[];
|
||||||
seed?: (ctx: ModuleBuildContext & { app: App }) => Promise<void>;
|
seed?: (ctx: ModuleBuildContext & { app: App }) => Promise<void>;
|
||||||
manager?: Omit<ModuleManagerOptions, "initial" | "onUpdated" | "seed">;
|
manager?: Omit<ModuleManagerOptions, "initial" | "onUpdated" | "seed">;
|
||||||
|
asyncEventsMode?: "sync" | "async" | "none";
|
||||||
|
drivers?: {
|
||||||
|
email?: IEmailDriver;
|
||||||
|
cache?: ICacheDriver;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
export type CreateAppConfig = {
|
export type CreateAppConfig = {
|
||||||
connection?:
|
connection?: Connection | { url: string };
|
||||||
| Connection
|
|
||||||
| {
|
|
||||||
// @deprecated
|
|
||||||
type: "libsql";
|
|
||||||
config: LibSqlCredentials;
|
|
||||||
}
|
|
||||||
| LibSqlCredentials;
|
|
||||||
initialConfig?: InitialModuleConfigs;
|
initialConfig?: InitialModuleConfigs;
|
||||||
options?: AppOptions;
|
options?: AppOptions;
|
||||||
};
|
};
|
||||||
@@ -52,53 +79,40 @@ export type CreateAppConfig = {
|
|||||||
export type AppConfig = InitialModuleConfigs;
|
export type AppConfig = InitialModuleConfigs;
|
||||||
export type LocalApiOptions = Request | ApiOptions;
|
export type LocalApiOptions = Request | ApiOptions;
|
||||||
|
|
||||||
export class App {
|
export class App<C extends Connection = Connection, Options extends AppOptions = AppOptions> {
|
||||||
modules: ModuleManager;
|
|
||||||
static readonly Events = AppEvents;
|
static readonly Events = AppEvents;
|
||||||
|
|
||||||
|
modules: ModuleManager;
|
||||||
adminController?: AdminController;
|
adminController?: AdminController;
|
||||||
|
_id: string = crypto.randomUUID();
|
||||||
|
plugins: Map<string, AppPluginConfig> = new Map();
|
||||||
|
drivers: Options["drivers"] = {};
|
||||||
|
|
||||||
private trigger_first_boot = false;
|
private trigger_first_boot = false;
|
||||||
private plugins: AppPlugin[];
|
|
||||||
private _id: string = crypto.randomUUID();
|
|
||||||
private _building: boolean = false;
|
private _building: boolean = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private connection: Connection,
|
public connection: C,
|
||||||
_initialConfig?: InitialModuleConfigs,
|
_initialConfig?: InitialModuleConfigs,
|
||||||
private options?: AppOptions,
|
private options?: Options,
|
||||||
) {
|
) {
|
||||||
this.plugins = options?.plugins ?? [];
|
this.drivers = options?.drivers ?? {};
|
||||||
|
|
||||||
|
for (const plugin of options?.plugins ?? []) {
|
||||||
|
const config = plugin(this);
|
||||||
|
if (this.plugins.has(config.name)) {
|
||||||
|
throw new Error(`Plugin ${config.name} already registered`);
|
||||||
|
}
|
||||||
|
this.plugins.set(config.name, config);
|
||||||
|
}
|
||||||
|
this.runPlugins("onBoot");
|
||||||
this.modules = new ModuleManager(connection, {
|
this.modules = new ModuleManager(connection, {
|
||||||
...(options?.manager ?? {}),
|
...(options?.manager ?? {}),
|
||||||
initial: _initialConfig,
|
initial: _initialConfig,
|
||||||
onUpdated: async (key, config) => {
|
onUpdated: this.onUpdated.bind(this),
|
||||||
// if the EventManager was disabled, we assume we shouldn't
|
onFirstBoot: this.onFirstBoot.bind(this),
|
||||||
// respond to events, such as "onUpdated".
|
onServerInit: this.onServerInit.bind(this),
|
||||||
// this is important if multiple changes are done, and then build() is called manually
|
onModulesBuilt: this.onModulesBuilt.bind(this),
|
||||||
if (!this.emgr.enabled) {
|
|
||||||
$console.warn("App config updated, but event manager is disabled, skip.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$console.log("App config updated", key);
|
|
||||||
// @todo: potentially double syncing
|
|
||||||
await this.build({ sync: true });
|
|
||||||
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this }));
|
|
||||||
},
|
|
||||||
onFirstBoot: async () => {
|
|
||||||
$console.log("App first boot");
|
|
||||||
this.trigger_first_boot = true;
|
|
||||||
},
|
|
||||||
onServerInit: async (server) => {
|
|
||||||
server.use(async (c, next) => {
|
|
||||||
c.set("app", this);
|
|
||||||
await next();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// gracefully add the app id
|
|
||||||
c.res.headers.set("X-bknd-id", this._id);
|
|
||||||
} catch (e) {}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
this.modules.ctx().emgr.registerEvents(AppEvents);
|
this.modules.ctx().emgr.registerEvents(AppEvents);
|
||||||
}
|
}
|
||||||
@@ -107,6 +121,32 @@ export class App {
|
|||||||
return this.modules.ctx().emgr;
|
return this.modules.ctx().emgr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async runPlugins<Key extends keyof AppPluginConfig>(
|
||||||
|
key: Key,
|
||||||
|
...args: any[]
|
||||||
|
): Promise<{ name: string; result: any }[]> {
|
||||||
|
const results: { name: string; result: any }[] = [];
|
||||||
|
for (const [name, config] of this.plugins) {
|
||||||
|
try {
|
||||||
|
if (key in config && config[key]) {
|
||||||
|
const fn = config[key];
|
||||||
|
if (fn && typeof fn === "function") {
|
||||||
|
$console.debug(`[Plugin:${name}] ${key}`);
|
||||||
|
// @ts-expect-error
|
||||||
|
const result = await fn(...args);
|
||||||
|
results.push({
|
||||||
|
name,
|
||||||
|
result,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$console.warn(`[Plugin:${name}] error running "${key}"`, String(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results as any;
|
||||||
|
}
|
||||||
|
|
||||||
async build(options?: { sync?: boolean; fetch?: boolean; forceBuild?: boolean }) {
|
async build(options?: { sync?: boolean; fetch?: boolean; forceBuild?: boolean }) {
|
||||||
// prevent multiple concurrent builds
|
// prevent multiple concurrent builds
|
||||||
if (this._building) {
|
if (this._building) {
|
||||||
@@ -115,6 +155,8 @@ export class App {
|
|||||||
}
|
}
|
||||||
if (!options?.forceBuild) return;
|
if (!options?.forceBuild) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.runPlugins("beforeBuild");
|
||||||
this._building = true;
|
this._building = true;
|
||||||
|
|
||||||
if (options?.sync) this.modules.ctx().flags.sync_required = true;
|
if (options?.sync) this.modules.ctx().flags.sync_required = true;
|
||||||
@@ -126,13 +168,10 @@ export class App {
|
|||||||
guard.registerPermissions(Object.values(SystemPermissions));
|
guard.registerPermissions(Object.values(SystemPermissions));
|
||||||
server.route("/api/system", new SystemController(this).getController());
|
server.route("/api/system", new SystemController(this).getController());
|
||||||
|
|
||||||
// load plugins
|
// emit built event
|
||||||
if (this.plugins.length > 0) {
|
|
||||||
await Promise.all(this.plugins.map((plugin) => plugin(this)));
|
|
||||||
}
|
|
||||||
|
|
||||||
$console.log("App built");
|
$console.log("App built");
|
||||||
await this.emgr.emit(new AppBuiltEvent({ app: this }));
|
await this.emgr.emit(new AppBuiltEvent({ app: this }));
|
||||||
|
await this.runPlugins("onBuilt");
|
||||||
|
|
||||||
// first boot is set from ModuleManager when there wasn't a config table
|
// first boot is set from ModuleManager when there wasn't a config table
|
||||||
if (this.trigger_first_boot) {
|
if (this.trigger_first_boot) {
|
||||||
@@ -160,7 +199,7 @@ export class App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get fetch(): Hono["fetch"] {
|
get fetch(): Hono["fetch"] {
|
||||||
return this.server.fetch;
|
return this.server.fetch as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
get module() {
|
get module() {
|
||||||
@@ -189,7 +228,10 @@ export class App {
|
|||||||
registerAdminController(config?: AdminControllerOptions) {
|
registerAdminController(config?: AdminControllerOptions) {
|
||||||
// register admin
|
// register admin
|
||||||
this.adminController = new AdminController(this, config);
|
this.adminController = new AdminController(this, config);
|
||||||
this.modules.server.route(config?.basepath ?? "/", this.adminController.getController());
|
this.modules.server.route(
|
||||||
|
this.adminController.basepath,
|
||||||
|
this.adminController.getController(),
|
||||||
|
);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,34 +255,82 @@ export class App {
|
|||||||
|
|
||||||
return new Api({ host: "http://localhost", ...(options ?? {}), fetcher });
|
return new Api({ host: "http://localhost", ...(options ?? {}), fetcher });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async onUpdated<Module extends keyof Modules>(module: Module, config: ModuleConfigs[Module]) {
|
||||||
|
// if the EventManager was disabled, we assume we shouldn't
|
||||||
|
// respond to events, such as "onUpdated".
|
||||||
|
// this is important if multiple changes are done, and then build() is called manually
|
||||||
|
if (!this.emgr.enabled) {
|
||||||
|
$console.warn("App config updated, but event manager is disabled, skip.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$console.log("App config updated", module);
|
||||||
|
// @todo: potentially double syncing
|
||||||
|
await this.build({ sync: true });
|
||||||
|
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this, module, config }));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async onFirstBoot() {
|
||||||
|
$console.log("App first boot");
|
||||||
|
this.trigger_first_boot = true;
|
||||||
|
await this.runPlugins("onFirstBoot");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async onServerInit(server: Hono<ServerEnv>) {
|
||||||
|
server.use(async (c, next) => {
|
||||||
|
c.set("app", this);
|
||||||
|
await this.emgr.emit(new AppRequest({ app: this, request: c.req.raw }));
|
||||||
|
await next();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// gracefully add the app id
|
||||||
|
c.res.headers.set("X-bknd-id", this._id);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
await this.emgr.emit(
|
||||||
|
new AppBeforeResponse({ app: this, request: c.req.raw, response: c.res }),
|
||||||
|
);
|
||||||
|
|
||||||
|
// execute collected async events (async by default)
|
||||||
|
switch (this.options?.asyncEventsMode ?? "async") {
|
||||||
|
case "sync":
|
||||||
|
await this.emgr.executeAsyncs();
|
||||||
|
break;
|
||||||
|
case "async":
|
||||||
|
this.emgr.executeAsyncs();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// call server init if set
|
||||||
|
if (this.options?.manager?.onServerInit) {
|
||||||
|
this.options.manager.onServerInit(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.runPlugins("onServerInit", server);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async onModulesBuilt(ctx: ModuleBuildContext) {
|
||||||
|
const results = (await this.runPlugins("schema")) as {
|
||||||
|
name: string;
|
||||||
|
result: ReturnType<typeof prototypeEm>;
|
||||||
|
}[];
|
||||||
|
if (results.length > 0) {
|
||||||
|
for (const { name, result } of results) {
|
||||||
|
if (result) {
|
||||||
|
$console.log(`[Plugin:${name}] schema`);
|
||||||
|
ctx.helper.ensureSchema(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createApp(config: CreateAppConfig = {}) {
|
export function createApp(config: CreateAppConfig = {}) {
|
||||||
let connection: Connection | undefined = undefined;
|
if (!config.connection || !Connection.isConnection(config.connection)) {
|
||||||
|
|
||||||
try {
|
|
||||||
if (Connection.isConnection(config.connection)) {
|
|
||||||
connection = config.connection;
|
|
||||||
} else if (typeof config.connection === "object") {
|
|
||||||
if ("type" in config.connection) {
|
|
||||||
$console.warn(
|
|
||||||
"Using deprecated connection type 'libsql', use the 'config' object directly.",
|
|
||||||
);
|
|
||||||
connection = new LibsqlConnection(config.connection.config);
|
|
||||||
} else {
|
|
||||||
connection = new LibsqlConnection(config.connection);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
connection = new LibsqlConnection({ url: ":memory:" });
|
|
||||||
$console.warn("No connection provided, using in-memory database");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
$console.error("Could not create connection", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!connection) {
|
|
||||||
throw new Error("Invalid connection");
|
throw new Error("Invalid connection");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new App(connection, config.initialConfig, config.options);
|
return new App(config.connection, config.initialConfig, config.options);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import type { TestRunner } from "core/test";
|
||||||
|
import type { BkndConfig, DefaultArgs, FrameworkOptions, RuntimeOptions } from "./index";
|
||||||
|
import type { App } from "App";
|
||||||
|
|
||||||
|
export function adapterTestSuite<
|
||||||
|
Config extends BkndConfig = BkndConfig,
|
||||||
|
Args extends DefaultArgs = DefaultArgs,
|
||||||
|
>(
|
||||||
|
testRunner: TestRunner,
|
||||||
|
{
|
||||||
|
makeApp,
|
||||||
|
makeHandler,
|
||||||
|
label = "app",
|
||||||
|
overrides = {},
|
||||||
|
}: {
|
||||||
|
makeApp: (
|
||||||
|
config: Config,
|
||||||
|
args?: Args,
|
||||||
|
opts?: RuntimeOptions | FrameworkOptions,
|
||||||
|
) => Promise<App>;
|
||||||
|
makeHandler?: (
|
||||||
|
config?: Config,
|
||||||
|
args?: Args,
|
||||||
|
opts?: RuntimeOptions | FrameworkOptions,
|
||||||
|
) => (request: Request) => Promise<Response>;
|
||||||
|
label?: string;
|
||||||
|
overrides?: {
|
||||||
|
dbUrl?: string;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const { test, expect, mock } = testRunner;
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
|
||||||
|
test(`creates ${label}`, async () => {
|
||||||
|
const beforeBuild = mock(async () => null) as any;
|
||||||
|
const onBuilt = mock(async () => null) as any;
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
app: (env) => ({
|
||||||
|
connection: { url: env.url },
|
||||||
|
initialConfig: {
|
||||||
|
server: { cors: { origin: env.origin } },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
beforeBuild,
|
||||||
|
onBuilt,
|
||||||
|
} as const satisfies BkndConfig;
|
||||||
|
|
||||||
|
const app = await makeApp(
|
||||||
|
config as any,
|
||||||
|
{
|
||||||
|
url: overrides.dbUrl ?? ":memory:",
|
||||||
|
origin: "localhost",
|
||||||
|
} as any,
|
||||||
|
{ id },
|
||||||
|
);
|
||||||
|
expect(app).toBeDefined();
|
||||||
|
expect(app.toJSON().server.cors.origin).toEqual("localhost");
|
||||||
|
expect(beforeBuild).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onBuilt).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (makeHandler) {
|
||||||
|
const getConfig = async (fetcher: (r: Request) => Promise<Response>) => {
|
||||||
|
const res = await fetcher(new Request("http://localhost:3000/api/system/config"));
|
||||||
|
const data = (await res.json()) as any;
|
||||||
|
return { res, data };
|
||||||
|
};
|
||||||
|
|
||||||
|
test("responds with the same app id", async () => {
|
||||||
|
const fetcher = makeHandler(undefined, undefined, { id });
|
||||||
|
|
||||||
|
const { res, data } = await getConfig(fetcher);
|
||||||
|
expect(res.ok).toBe(true);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(data.server.cors.origin).toEqual("localhost");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("creates fresh & responds to api config", async () => {
|
||||||
|
// set the same id, but force recreate
|
||||||
|
const fetcher = makeHandler(undefined, undefined, { id, force: true });
|
||||||
|
|
||||||
|
const { res, data } = await getConfig(fetcher);
|
||||||
|
expect(res.ok).toBe(true);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(data.server.cors.origin).toEqual("*");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { afterAll, beforeAll, describe } from "bun:test";
|
||||||
|
import * as astro from "./astro.adapter";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||||
|
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||||
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
|
|
||||||
|
beforeAll(disableConsoleLog);
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
|
describe("astro adapter", () => {
|
||||||
|
adapterTestSuite(bunTestRunner, {
|
||||||
|
makeApp: astro.getApp,
|
||||||
|
makeHandler: (c, a, o) => (request: Request) => astro.serve(c, a, o)({ request }),
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,34 +1,25 @@
|
|||||||
import type { App } from "bknd";
|
import { type FrameworkBkndConfig, createFrameworkApp, type FrameworkOptions } from "bknd/adapter";
|
||||||
import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter";
|
|
||||||
import { Api, type ApiOptions } from "bknd/client";
|
|
||||||
|
|
||||||
export type AstroBkndConfig<Args = TAstro> = FrameworkBkndConfig<Args>;
|
|
||||||
|
|
||||||
|
type AstroEnv = NodeJS.ProcessEnv;
|
||||||
type TAstro = {
|
type TAstro = {
|
||||||
request: Request;
|
request: Request;
|
||||||
};
|
};
|
||||||
|
export type AstroBkndConfig<Env = AstroEnv> = FrameworkBkndConfig<Env>;
|
||||||
|
|
||||||
export type Options = {
|
export async function getApp<Env = AstroEnv>(
|
||||||
mode?: "static" | "dynamic";
|
config: AstroBkndConfig<Env> = {},
|
||||||
} & Omit<ApiOptions, "host"> & {
|
args: Env = {} as Env,
|
||||||
host?: string;
|
opts: FrameworkOptions = {},
|
||||||
};
|
) {
|
||||||
|
return await createFrameworkApp(config, args ?? import.meta.env, opts);
|
||||||
export async function getApi(Astro: TAstro, options: Options = { mode: "static" }) {
|
|
||||||
const api = new Api({
|
|
||||||
host: new URL(Astro.request.url).origin,
|
|
||||||
headers: options.mode === "dynamic" ? Astro.request.headers : undefined,
|
|
||||||
});
|
|
||||||
await api.verifyAuth();
|
|
||||||
return api;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let app: App;
|
export function serve<Env = AstroEnv>(
|
||||||
export function serve<Context extends TAstro = TAstro>(config: AstroBkndConfig<Context> = {}) {
|
config: AstroBkndConfig<Env> = {},
|
||||||
return async (args: Context) => {
|
args: Env = {} as Env,
|
||||||
if (!app) {
|
opts?: FrameworkOptions,
|
||||||
app = await createFrameworkApp(config, args);
|
) {
|
||||||
}
|
return async (fnArgs: TAstro) => {
|
||||||
return app.fetch(args.request);
|
return (await getApp(config, args, opts)).fetch(fnArgs.request);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,68 +1,76 @@
|
|||||||
import type { App } from "bknd";
|
import type { App } from "bknd";
|
||||||
import { handle } from "hono/aws-lambda";
|
import { handle } from "hono/aws-lambda";
|
||||||
import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter";
|
import { serveStatic } from "@hono/node-server/serve-static";
|
||||||
|
import { type RuntimeBkndConfig, createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
|
||||||
|
|
||||||
export type AwsLambdaBkndConfig = RuntimeBkndConfig & {
|
type AwsLambdaEnv = object;
|
||||||
assets?:
|
export type AwsLambdaBkndConfig<Env extends AwsLambdaEnv = AwsLambdaEnv> =
|
||||||
| {
|
RuntimeBkndConfig<Env> & {
|
||||||
mode: "local";
|
assets?:
|
||||||
root: string;
|
| {
|
||||||
}
|
mode: "local";
|
||||||
| {
|
root: string;
|
||||||
mode: "url";
|
}
|
||||||
url: string;
|
| {
|
||||||
};
|
mode: "url";
|
||||||
};
|
url: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
let app: App;
|
export async function createApp<Env extends AwsLambdaEnv = AwsLambdaEnv>(
|
||||||
export async function createApp({
|
{ adminOptions = false, assets, ...config }: AwsLambdaBkndConfig<Env> = {},
|
||||||
adminOptions = false,
|
args: Env = {} as Env,
|
||||||
assets,
|
opts?: RuntimeOptions,
|
||||||
...config
|
): Promise<App> {
|
||||||
}: AwsLambdaBkndConfig = {}) {
|
let additional: Partial<RuntimeBkndConfig> = {
|
||||||
if (!app) {
|
adminOptions,
|
||||||
let additional: Partial<RuntimeBkndConfig> = {
|
};
|
||||||
adminOptions,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (assets?.mode) {
|
if (assets?.mode) {
|
||||||
switch (assets.mode) {
|
switch (assets.mode) {
|
||||||
case "local":
|
case "local":
|
||||||
// @todo: serve static outside app context
|
// @todo: serve static outside app context
|
||||||
additional = {
|
additional = {
|
||||||
adminOptions: adminOptions === false ? undefined : adminOptions,
|
adminOptions: adminOptions === false ? undefined : adminOptions,
|
||||||
serveStatic: (await import("@hono/node-server/serve-static")).serveStatic({
|
serveStatic: serveStatic({
|
||||||
root: assets.root,
|
root: assets.root,
|
||||||
onFound: (path, c) => {
|
onFound: (path, c) => {
|
||||||
c.res.headers.set("Cache-Control", "public, max-age=31536000");
|
c.res.headers.set("Cache-Control", "public, max-age=31536000");
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case "url":
|
case "url":
|
||||||
additional.adminOptions = {
|
additional.adminOptions = {
|
||||||
...(typeof adminOptions === "object" ? adminOptions : {}),
|
...(typeof adminOptions === "object" ? adminOptions : {}),
|
||||||
assets_path: assets.url,
|
assetsPath: assets.url,
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error("Invalid assets mode");
|
throw new Error("Invalid assets mode");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
app = await createRuntimeApp({
|
|
||||||
...config,
|
|
||||||
...additional,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return app;
|
return await createRuntimeApp(
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
...additional,
|
||||||
|
},
|
||||||
|
args ?? process.env,
|
||||||
|
opts,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serveLambda(config: AwsLambdaBkndConfig = {}) {
|
export function serve<Env extends AwsLambdaEnv = AwsLambdaEnv>(
|
||||||
console.log("serving lambda");
|
config: AwsLambdaBkndConfig<Env> = {},
|
||||||
|
args: Env = {} as Env,
|
||||||
|
opts?: RuntimeOptions,
|
||||||
|
) {
|
||||||
return async (event) => {
|
return async (event) => {
|
||||||
const app = await createApp(config);
|
const app = await createApp(config, args, opts);
|
||||||
return await handle(app.server)(event);
|
return await handle(app.server)(event);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// compatibility with old code
|
||||||
|
export const serveLambda = serve;
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { afterAll, beforeAll, describe } from "bun:test";
|
||||||
|
import * as awsLambda from "./aws-lambda.adapter";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||||
|
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||||
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
|
|
||||||
|
beforeAll(disableConsoleLog);
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
|
describe("aws adapter", () => {
|
||||||
|
adapterTestSuite(bunTestRunner, {
|
||||||
|
makeApp: awsLambda.createApp,
|
||||||
|
// @todo: add a request to lambda event translator?
|
||||||
|
makeHandler: (c, a, o) => async (request: Request) => {
|
||||||
|
const app = await awsLambda.createApp(c, a, o);
|
||||||
|
return app.fetch(request);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { afterAll, beforeAll, describe } from "bun:test";
|
||||||
|
import * as bun from "./bun.adapter";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||||
|
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||||
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
|
|
||||||
|
beforeAll(disableConsoleLog);
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
|
describe("bun adapter", () => {
|
||||||
|
adapterTestSuite(bunTestRunner, {
|
||||||
|
makeApp: bun.createApp,
|
||||||
|
makeHandler: bun.createHandler,
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,47 +1,68 @@
|
|||||||
/// <reference types="bun-types" />
|
/// <reference types="bun-types" />
|
||||||
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import type { App } from "bknd";
|
import { type RuntimeBkndConfig, createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
|
||||||
import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter";
|
import { registerLocalMediaAdapter } from ".";
|
||||||
import { registerLocalMediaAdapter } from "bknd/adapter/node";
|
|
||||||
import { config } from "bknd/core";
|
import { config } from "bknd/core";
|
||||||
import type { ServeOptions } from "bun";
|
import type { ServeOptions } from "bun";
|
||||||
import { serveStatic } from "hono/bun";
|
import { serveStatic } from "hono/bun";
|
||||||
|
import type { App } from "App";
|
||||||
|
|
||||||
let app: App;
|
type BunEnv = Bun.Env;
|
||||||
|
export type BunBkndConfig<Env = BunEnv> = RuntimeBkndConfig<Env> & Omit<ServeOptions, "fetch">;
|
||||||
|
|
||||||
export type BunBkndConfig = RuntimeBkndConfig & Omit<ServeOptions, "fetch">;
|
export async function createApp<Env = BunEnv>(
|
||||||
|
{ distPath, ...config }: BunBkndConfig<Env> = {},
|
||||||
export async function createApp({ distPath, ...config }: RuntimeBkndConfig = {}) {
|
args: Env = {} as Env,
|
||||||
|
opts?: RuntimeOptions,
|
||||||
|
) {
|
||||||
const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static");
|
const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static");
|
||||||
|
registerLocalMediaAdapter();
|
||||||
|
|
||||||
if (!app) {
|
return await createRuntimeApp(
|
||||||
registerLocalMediaAdapter();
|
{
|
||||||
app = await createRuntimeApp({
|
|
||||||
...config,
|
...config,
|
||||||
serveStatic: serveStatic({ root }),
|
serveStatic: serveStatic({ root }),
|
||||||
});
|
},
|
||||||
}
|
args ?? (process.env as Env),
|
||||||
|
opts,
|
||||||
return app;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serve({
|
export function createHandler<Env = BunEnv>(
|
||||||
distPath,
|
config: BunBkndConfig<Env> = {},
|
||||||
connection,
|
args: Env = {} as Env,
|
||||||
initialConfig,
|
opts?: RuntimeOptions,
|
||||||
options,
|
) {
|
||||||
port = config.server.default_port,
|
let app: App | undefined;
|
||||||
onBuilt,
|
return async (req: Request) => {
|
||||||
buildConfig,
|
if (!app) {
|
||||||
adminOptions,
|
app = await createApp(config, args ?? (process.env as Env), opts);
|
||||||
...serveOptions
|
}
|
||||||
}: BunBkndConfig = {}) {
|
return app.fetch(req);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serve<Env = BunEnv>(
|
||||||
|
{
|
||||||
|
distPath,
|
||||||
|
connection,
|
||||||
|
initialConfig,
|
||||||
|
options,
|
||||||
|
port = config.server.default_port,
|
||||||
|
onBuilt,
|
||||||
|
buildConfig,
|
||||||
|
adminOptions,
|
||||||
|
...serveOptions
|
||||||
|
}: BunBkndConfig<Env> = {},
|
||||||
|
args: Env = {} as Env,
|
||||||
|
opts?: RuntimeOptions,
|
||||||
|
) {
|
||||||
Bun.serve({
|
Bun.serve({
|
||||||
...serveOptions,
|
...serveOptions,
|
||||||
port,
|
port,
|
||||||
fetch: async (request: Request) => {
|
fetch: createHandler(
|
||||||
const app = await createApp({
|
{
|
||||||
connection,
|
connection,
|
||||||
initialConfig,
|
initialConfig,
|
||||||
options,
|
options,
|
||||||
@@ -49,10 +70,11 @@ export function serve({
|
|||||||
buildConfig,
|
buildConfig,
|
||||||
adminOptions,
|
adminOptions,
|
||||||
distPath,
|
distPath,
|
||||||
});
|
},
|
||||||
return app.fetch(request);
|
args,
|
||||||
},
|
opts,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Server is running on http://localhost:${port}`);
|
console.info(`Server is running on http://localhost:${port}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { connectionTestSuite } from "data/connection/connection-test-suite";
|
||||||
|
import { bunSqlite } from "./BunSqliteConnection";
|
||||||
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
|
import { describe } from "bun:test";
|
||||||
|
import { Database } from "bun:sqlite";
|
||||||
|
|
||||||
|
describe("BunSqliteConnection", () => {
|
||||||
|
connectionTestSuite(bunTestRunner, {
|
||||||
|
makeConnection: () => ({
|
||||||
|
connection: bunSqlite({ database: new Database(":memory:") }),
|
||||||
|
dispose: async () => {},
|
||||||
|
}),
|
||||||
|
rawDialectDetails: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Database } from "bun:sqlite";
|
||||||
|
import { genericSqlite, type GenericSqliteConnection } from "bknd/data";
|
||||||
|
|
||||||
|
export type BunSqliteConnection = GenericSqliteConnection<Database>;
|
||||||
|
export type BunSqliteConnectionConfig = {
|
||||||
|
database: Database;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function bunSqlite(config?: BunSqliteConnectionConfig | { url: string }) {
|
||||||
|
let db: Database;
|
||||||
|
if (config) {
|
||||||
|
if ("database" in config) {
|
||||||
|
db = config.database;
|
||||||
|
} else {
|
||||||
|
db = new Database(config.url);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
db = new Database(":memory:");
|
||||||
|
}
|
||||||
|
|
||||||
|
return genericSqlite("bun-sqlite", db, (utils) => {
|
||||||
|
//const fn = cache ? "query" : "prepare";
|
||||||
|
const getStmt = (sql: string) => db.prepare(sql);
|
||||||
|
|
||||||
|
return {
|
||||||
|
db,
|
||||||
|
query: utils.buildQueryFn({
|
||||||
|
all: (sql, parameters) => getStmt(sql).all(...(parameters || [])),
|
||||||
|
run: (sql, parameters) => {
|
||||||
|
const { changes, lastInsertRowid } = getStmt(sql).run(...(parameters || []));
|
||||||
|
return {
|
||||||
|
insertId: utils.parseBigInt(lastInsertRowid),
|
||||||
|
numAffectedRows: utils.parseBigInt(changes),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
close: () => db.close(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1 +1,3 @@
|
|||||||
export * from "./bun.adapter";
|
export * from "./bun.adapter";
|
||||||
|
export * from "../node/storage";
|
||||||
|
export * from "./connection/BunSqliteConnection";
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user