mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 16:16:02 +00:00
Compare commits
292 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4758f41a02 | |||
| bebf9e0411 | |||
| ca55503e66 | |||
| 1ec3ba0e92 | |||
| ea372a8125 | |||
| 847c264b35 | |||
| b4f024bef8 | |||
| 8bc63ba2a7 | |||
| cecb0e829a | |||
| 491bd65da1 | |||
| ba7fea5571 | |||
| ba7587aa3f | |||
| 0a49061549 | |||
| 682ecc6dac | |||
| efbc5f115f | |||
| b4d6b1c04d | |||
| e21f1a3ee2 | |||
| a9efee3555 | |||
| 2ca1f64ae6 | |||
| 2b32a836cd | |||
| 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 | |||
| 01cb9d21b0 | |||
| c5ce79102e | |||
| 67ab3d2e38 | |||
| b1b88e37f1 | |||
| 7e61ff9995 | |||
| 58f9ea4cd7 | |||
| dde5ad365e | |||
| dda02807c1 | |||
| ef629321ab | |||
| 4f52537ea0 | |||
| e82b72275b | |||
| ab73b02138 | |||
| 36fba4588f | |||
| ad353e59c9 | |||
| 675a39ad5c | |||
| 1a72b9e84b | |||
| 27a282ea6b | |||
| 6740e20f72 | |||
| 6c1eea76e5 | |||
| 5ca21b6c01 | |||
| 00f52eb096 | |||
| 09074f6591 | |||
| 2d5da63eb2 | |||
| 3bf3bb32a4 | |||
| 3e28124494 | |||
| dd48962901 | |||
| 7743f71a11 | |||
| 88b5359f1c | |||
| 6acbe1189e | |||
| 7c1fb86b20 | |||
| 36e033b0de | |||
| 4ca68e837c | |||
| 9a683c8e35 | |||
| 4a2dbf8f79 | |||
| 4d75cfe0ea | |||
| b6d28bbf42 | |||
| 2a9c1be151 | |||
| d4a6a9326f | |||
| de854eec3a | |||
| 253174c14e | |||
| 9a741f49a0 | |||
| b8f87a2097 | |||
| 4625151db8 | |||
| 280d2dccfb | |||
| 3015385290 | |||
| 147d6b7ff7 | |||
| 82fba39684 | |||
| f801d3a556 | |||
| 837b0a3d43 | |||
| 6f776aeff7 |
@@ -0,0 +1,39 @@
|
||||
name: Run Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22.x"
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: "1.2.19"
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./app
|
||||
run: bun install
|
||||
|
||||
- name: Build
|
||||
working-directory: ./app
|
||||
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
|
||||
+3
-1
@@ -29,4 +29,6 @@ packages/media/.env
|
||||
.idea
|
||||
.vscode
|
||||
.git_old
|
||||
docker/tmp
|
||||
docker/tmp
|
||||
.debug
|
||||
.history
|
||||
@@ -1,30 +1,39 @@
|
||||
[](https://npmjs.org/package/bknd)
|
||||
[](https://www.npmjs.com/package/bknd)
|
||||
|
||||

|
||||
|
||||
<p align="center" width="100%">
|
||||
<a href="https://stackblitz.com/github/bknd-io/bknd-examples?hideExplorer=1&embed=1&view=preview&startScript=example-admin-rich&initialPath=%2Fdata%2Fschema">
|
||||
<a href="https://stackblitz.com/github/bknd-io/bknd-examples?hideExplorer=1&embed=1&view=preview&startScript=example-admin-rich&initialPath=%2Fdata%2Fschema" target="_blank">
|
||||
<strong>⭐ Live Demo</strong>
|
||||
</a>
|
||||
</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!
|
||||
* **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.**
|
||||
|
||||
> [!WARNING]
|
||||
> This project requires Node.js 22 or higher (because of `node:sqlite`).
|
||||
>
|
||||
> Please keep in mind that **bknd** is still under active development
|
||||
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
|
||||
|
||||
## 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.
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
@@ -40,7 +49,7 @@ Creating digital products always requires developing both the backend (the logic
|
||||
* 🏃♂️ Multiple run modes
|
||||
* standalone using the CLI
|
||||
* 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
|
||||
* ⚛️ React elements for auto-configured authentication and media components
|
||||
|
||||
@@ -78,7 +87,7 @@ export default function AdminPage() {
|
||||
### Using the REST API or TypeScript SDK (`bknd/client`)
|
||||
If you're not using a JavaScript environment, you can still access any endpoint using the REST API:
|
||||
```bash
|
||||
curl -XGET <your-endpoint>/api/data/<entity>
|
||||
curl -XGET <your-endpoint>/api/data/entity/<entity>
|
||||
{
|
||||
"data": [
|
||||
{ "id": 1, ... },
|
||||
|
||||
@@ -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 { App } from "../src";
|
||||
import { afterEach, describe, test, expect } from "bun:test";
|
||||
import { App, createApp } from "core/test/utils";
|
||||
import { getDummyConnection } from "./helper";
|
||||
import { Hono } from "hono";
|
||||
import * as proto from "../src/data/prototype";
|
||||
import { pick } from "lodash-es";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
afterAll(afterAllCleanup);
|
||||
afterEach(afterAllCleanup);
|
||||
|
||||
describe("App tests", async () => {
|
||||
test("boots and pongs", async () => {
|
||||
const app = new App(dummyConnection);
|
||||
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"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
tmp/*
|
||||
tmp/*
|
||||
!tmp/.gitkeep
|
||||
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",
|
||||
});
|
||||
});
|
||||
@@ -19,8 +19,8 @@ describe("Api", async () => {
|
||||
const token = await sign({ foo: "bar" }, "1234");
|
||||
const request = new Request("http://example.com/test", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
const api = new Api({ request });
|
||||
expect(api.isAuthVerified()).toBe(false);
|
||||
@@ -35,8 +35,8 @@ describe("Api", async () => {
|
||||
const token = await sign({ foo: "bar" }, "1234");
|
||||
const request = new Request("http://example.com/test", {
|
||||
headers: {
|
||||
Cookie: `auth=${token}`
|
||||
}
|
||||
Cookie: `auth=${token}`,
|
||||
},
|
||||
});
|
||||
const api = new Api({ request });
|
||||
expect(api.isAuthVerified()).toBe(false);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||
import { Guard } from "../../src/auth";
|
||||
import { parse } from "../../src/core/utils";
|
||||
import { Guard } from "../../src/auth/authorize/Guard";
|
||||
import { DataApi } from "../../src/data/api/DataApi";
|
||||
import { DataController } from "../../src/data/api/DataController";
|
||||
import { dataConfigSchema } from "../../src/data/data-schema";
|
||||
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 "core/utils/schema";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
@@ -26,7 +27,7 @@ describe("DataApi", () => {
|
||||
|
||||
it("returns result", async () => {
|
||||
const schema = proto.em({
|
||||
posts: proto.entity("posts", { title: proto.text() })
|
||||
posts: proto.entity("posts", { title: proto.text() }),
|
||||
});
|
||||
const em = schemaToEm(schema);
|
||||
await em.schema().sync({ force: true });
|
||||
@@ -40,19 +41,17 @@ describe("DataApi", () => {
|
||||
|
||||
{
|
||||
const res = (await app.request("/entity/posts")) as Response;
|
||||
const { data } = await res.json();
|
||||
const { data } = (await res.json()) as any;
|
||||
expect(data.length).toEqual(3);
|
||||
}
|
||||
|
||||
// @ts-ignore tests
|
||||
const api = new DataApi({ basepath: "/", queryLengthLimit: 50 });
|
||||
// @ts-ignore protected
|
||||
api.fetcher = app.request as typeof fetch;
|
||||
const api = new DataApi({ basepath: "/", queryLengthLimit: 50 }, app.request as typeof fetch);
|
||||
{
|
||||
const req = api.readMany("posts", { select: ["title"] });
|
||||
expect(req.request.method).toBe("GET");
|
||||
const res = await req;
|
||||
expect(res.data).toEqual(payload);
|
||||
expect(res.data).toEqual(payload as any);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -60,11 +59,164 @@ describe("DataApi", () => {
|
||||
select: ["title"],
|
||||
limit: 100000,
|
||||
offset: 0,
|
||||
sort: "id"
|
||||
sort: "id",
|
||||
});
|
||||
expect(req.request.method).toBe("POST");
|
||||
const res = await req;
|
||||
expect(res.data).toEqual(payload);
|
||||
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 () => {
|
||||
const schema = proto.em({
|
||||
posts: proto.entity("posts", { title: proto.text(), count: proto.number() }),
|
||||
});
|
||||
const em = schemaToEm(schema);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
const payload = [
|
||||
{ title: "foo", count: 0 },
|
||||
{ title: "bar", count: 0 },
|
||||
{ title: "baz", count: 0 },
|
||||
{ title: "bla", count: 2 },
|
||||
];
|
||||
await em.mutator("posts").insertMany(payload);
|
||||
|
||||
const ctx: any = { em, guard: new Guard() };
|
||||
const controller = new DataController(ctx, dataConfig);
|
||||
const app = controller.getController();
|
||||
|
||||
// @ts-ignore tests
|
||||
const api = new DataApi({ basepath: "/" }, app.request as typeof fetch);
|
||||
{
|
||||
const req = api.readMany("posts", {
|
||||
select: ["title", "count"],
|
||||
});
|
||||
const res = await req;
|
||||
expect(res.data).toEqual(payload as any);
|
||||
}
|
||||
|
||||
{
|
||||
// update with empty where
|
||||
expect(() => api.updateMany("posts", {}, { count: 1 })).toThrow();
|
||||
expect(() => api.updateMany("posts", undefined, { count: 1 })).toThrow();
|
||||
}
|
||||
|
||||
{
|
||||
// update
|
||||
const req = await api.updateMany("posts", { count: 0 }, { count: 1 });
|
||||
expect(req.res.status).toBe(200);
|
||||
}
|
||||
|
||||
{
|
||||
// compare
|
||||
const res = await api.readMany("posts", {
|
||||
select: ["title", "count"],
|
||||
});
|
||||
expect(res.map((p) => p.count)).toEqual([1, 1, 1, 2]);
|
||||
}
|
||||
});
|
||||
|
||||
it("refines", async () => {
|
||||
const schema = proto.em({
|
||||
posts: proto.entity("posts", { title: proto.text() }),
|
||||
});
|
||||
const em = schemaToEm(schema);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
const payload = [{ title: "foo" }, { title: "bar" }, { title: "baz" }];
|
||||
await em.mutator("posts").insertMany(payload);
|
||||
|
||||
const ctx: any = { em, guard: new Guard() };
|
||||
const controller = new DataController(ctx, dataConfig);
|
||||
const app = controller.getController();
|
||||
|
||||
const api = new DataApi({ basepath: "/" }, app.request as typeof fetch);
|
||||
const normalOne = api.readOne("posts", 1);
|
||||
const normal = api.readMany("posts", { select: ["title"], where: { title: "baz" } });
|
||||
expect((await normal).data).toEqual([{ title: "baz" }] as any);
|
||||
|
||||
// refine
|
||||
const refined = normal.refine((data) => data[0]);
|
||||
expect((await refined).data).toEqual({ title: "baz" } as any);
|
||||
|
||||
// one
|
||||
const oneBy = api.readOneBy("posts", { where: { title: "baz" }, select: ["title"] });
|
||||
const oneByRes = await oneBy;
|
||||
expect(oneByRes.data).toEqual({ title: "baz" } as any);
|
||||
expect(oneByRes.body.meta.items).toEqual(1);
|
||||
});
|
||||
|
||||
it("exists/count", async () => {
|
||||
const schema = proto.em({
|
||||
posts: proto.entity("posts", { title: proto.text() }),
|
||||
});
|
||||
const em = schemaToEm(schema);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
const payload = [{ title: "foo" }, { title: "bar" }, { title: "baz" }];
|
||||
await em.mutator("posts").insertMany(payload);
|
||||
|
||||
const ctx: any = { em, guard: new Guard() };
|
||||
const controller = new DataController(ctx, dataConfig);
|
||||
const app = controller.getController();
|
||||
|
||||
const api = new DataApi({ basepath: "/" }, app.request as typeof fetch);
|
||||
|
||||
const exists = api.exists("posts", { id: 1 });
|
||||
expect((await exists).exists).toBeTrue();
|
||||
|
||||
expect((await api.count("posts")).count).toEqual(3);
|
||||
});
|
||||
|
||||
it("creates many", async () => {
|
||||
const schema = proto.em({
|
||||
posts: proto.entity("posts", { title: proto.text(), count: proto.number() }),
|
||||
});
|
||||
const em = schemaToEm(schema);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
const payload = [
|
||||
{ title: "foo", count: 0 },
|
||||
{ title: "bar", count: 0 },
|
||||
{ title: "baz", count: 0 },
|
||||
{ title: "bla", count: 2 },
|
||||
];
|
||||
|
||||
const ctx: any = { em, guard: new Guard() };
|
||||
const controller = new DataController(ctx, dataConfig);
|
||||
const app = controller.getController();
|
||||
|
||||
// @ts-ignore tests
|
||||
const api = new DataApi({ basepath: "/" }, app.request as typeof fetch);
|
||||
|
||||
{
|
||||
// create many
|
||||
const res = await api.createMany("posts", payload);
|
||||
expect(res.data?.length).toEqual(4);
|
||||
expect(res.ok).toBeTrue();
|
||||
}
|
||||
|
||||
{
|
||||
const req = api.readMany("posts", {
|
||||
select: ["title", "count"],
|
||||
});
|
||||
const res = await req;
|
||||
expect(res.data).toEqual(payload as any);
|
||||
}
|
||||
|
||||
{
|
||||
// create with empty
|
||||
expect(() => api.createMany("posts", [])).toThrow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/// <reference types="@types/bun" />
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { Hono } from "hono";
|
||||
import { getFileFromContext, isFile, isReadableStream } from "../../src/core/utils";
|
||||
import { MediaApi } from "../../src/media/api/MediaApi";
|
||||
import { getFileFromContext, isFile, isReadableStream } from "core/utils";
|
||||
import { MediaApi } from "media/api/MediaApi";
|
||||
import { assetsPath, assetsTmpPath } from "../helper";
|
||||
|
||||
const mockedBackend = new Hono()
|
||||
@@ -18,8 +18,8 @@ const mockedBackend = new Hono()
|
||||
return new Response(file, {
|
||||
headers: {
|
||||
"Content-Type": file.type,
|
||||
"Content-Length": file.size.toString()
|
||||
}
|
||||
"Content-Length": file.size.toString(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,19 +30,37 @@ describe("MediaApi", () => {
|
||||
// @ts-ignore tests
|
||||
const api = new MediaApi({
|
||||
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", () => {
|
||||
// @ts-ignore tests
|
||||
const api = new MediaApi({
|
||||
token: "token"
|
||||
token: "token",
|
||||
token_transport: "header",
|
||||
});
|
||||
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 () => {
|
||||
const name = "image.png";
|
||||
const path = `${assetsTmpPath}/${name}`;
|
||||
@@ -103,8 +121,12 @@ describe("MediaApi", () => {
|
||||
});
|
||||
|
||||
it("should upload file in various ways", async () => {
|
||||
// @ts-ignore tests
|
||||
const api = new MediaApi({}, mockedBackend.request);
|
||||
const api = new MediaApi(
|
||||
{
|
||||
upload_fetcher: mockedBackend.request,
|
||||
},
|
||||
mockedBackend.request,
|
||||
);
|
||||
const file = Bun.file(`${assetsPath}/image.png`);
|
||||
|
||||
async function matches(req: Promise<any>, filename: string) {
|
||||
@@ -139,7 +161,7 @@ describe("MediaApi", () => {
|
||||
const response = (await mockedBackend.request(url)) as Response;
|
||||
await matches(
|
||||
await api.upload(response.body!, { filename: "readable.png" }),
|
||||
"readable.png"
|
||||
"readable.png",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -61,7 +61,7 @@ describe("ModuleApi", () => {
|
||||
|
||||
it("adds additional headers from options", () => {
|
||||
const headers = new Headers({
|
||||
"X-Test": "123"
|
||||
"X-Test": "123",
|
||||
});
|
||||
const api = new Api({ host, headers });
|
||||
expect(api.get("/").request.headers.get("X-Test")).toEqual("123");
|
||||
@@ -75,7 +75,7 @@ describe("ModuleApi", () => {
|
||||
it("uses search params", () => {
|
||||
const api = new Api({ host });
|
||||
const search = new URLSearchParams({
|
||||
foo: "bar"
|
||||
foo: "bar",
|
||||
});
|
||||
expect(api.get("/", search).request.url).toEqual("http://localhost/?" + search.toString());
|
||||
});
|
||||
@@ -89,6 +89,14 @@ describe("ModuleApi", () => {
|
||||
expect(api.delete("/").request.method).toEqual("DELETE");
|
||||
});
|
||||
|
||||
it("refines", async () => {
|
||||
const app = new Hono().get("/endpoint", (c) => c.json({ foo: ["bar"] }));
|
||||
const api = new Api({ host }, app.request as typeof fetch);
|
||||
|
||||
expect((await api.get("/endpoint")).data).toEqual({ foo: ["bar"] });
|
||||
expect((await api.get("/endpoint").refine((data) => data.foo)).data).toEqual(["bar"]);
|
||||
});
|
||||
|
||||
// @todo: test error response
|
||||
// @todo: test method shortcut functions
|
||||
});
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { describe, expect, mock, test } from "bun:test";
|
||||
import type { ModuleBuildContext } from "../../src";
|
||||
import { App, createApp } from "core/test/utils";
|
||||
import * as proto from "../../src/data/prototype";
|
||||
|
||||
describe("App", () => {
|
||||
test("seed includes ctx and app", async () => {
|
||||
const called = mock(() => null);
|
||||
await createApp({
|
||||
options: {
|
||||
seed: async ({ app, ...ctx }) => {
|
||||
called();
|
||||
expect(app).toBeDefined();
|
||||
expect(ctx).toBeDefined();
|
||||
expect(Object.keys(ctx)).toEqual([
|
||||
"connection",
|
||||
"server",
|
||||
"em",
|
||||
"emgr",
|
||||
"guard",
|
||||
"flags",
|
||||
"logger",
|
||||
"helper",
|
||||
]);
|
||||
},
|
||||
},
|
||||
}).build();
|
||||
expect(called).toHaveBeenCalled();
|
||||
|
||||
const app = createApp({
|
||||
initialConfig: {
|
||||
data: proto
|
||||
.em({
|
||||
todos: proto.entity("todos", {
|
||||
title: proto.text(),
|
||||
}),
|
||||
})
|
||||
.toJSON(),
|
||||
},
|
||||
options: {
|
||||
//manager: { verbosity: 2 },
|
||||
seed: async ({ app, ...ctx }: ModuleBuildContext & { app: App }) => {
|
||||
await ctx.em.mutator("todos").insertOne({ title: "ctx" });
|
||||
await app.getApi().data.createOne("todos", { title: "api" });
|
||||
},
|
||||
},
|
||||
});
|
||||
await app.build();
|
||||
|
||||
const todos = await app.getApi().data.readMany("todos");
|
||||
expect(todos.length).toBe(2);
|
||||
expect(todos[0]?.title).toBe("ctx");
|
||||
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,37 @@
|
||||
import { AppServer, serverConfigSchema } from "modules/server/AppServer";
|
||||
import { describe, test, expect } from "bun:test";
|
||||
|
||||
describe("AppServer", () => {
|
||||
test("config", () => {
|
||||
{
|
||||
const server = new AppServer();
|
||||
expect(server).toBeDefined();
|
||||
expect(server.config).toEqual({
|
||||
cors: {
|
||||
allow_credentials: true,
|
||||
origin: "*",
|
||||
allow_methods: ["GET", "POST", "PATCH", "PUT", "DELETE"],
|
||||
allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const server = new AppServer({
|
||||
cors: {
|
||||
origin: "https",
|
||||
allow_methods: ["GET", "POST"],
|
||||
},
|
||||
});
|
||||
expect(server).toBeDefined();
|
||||
expect(server.config).toEqual({
|
||||
cors: {
|
||||
allow_credentials: true,
|
||||
origin: "https",
|
||||
allow_methods: ["GET", "POST"],
|
||||
allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"],
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,8 @@
|
||||
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 { StorageLocalAdapter } from "../../src/media/storage/adapters/StorageLocalAdapter";
|
||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||
|
||||
describe("repros", async () => {
|
||||
/**
|
||||
@@ -24,9 +25,9 @@ describe("repros", async () => {
|
||||
adapter: {
|
||||
type: "local",
|
||||
config: {
|
||||
path: "./"
|
||||
}
|
||||
}
|
||||
path: "./",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(config.enabled).toBe(true);
|
||||
@@ -38,9 +39,9 @@ describe("repros", async () => {
|
||||
"entities.test",
|
||||
proto
|
||||
.entity("test", {
|
||||
content: proto.text()
|
||||
content: proto.text(),
|
||||
})
|
||||
.toJSON()
|
||||
.toJSON(),
|
||||
);
|
||||
expect(app.em.entities.map((e) => e.name)).toContain("test");
|
||||
}
|
||||
@@ -54,8 +55,8 @@ describe("repros", async () => {
|
||||
hidden: false,
|
||||
mime_types: [],
|
||||
virtual: true,
|
||||
entity: "test"
|
||||
}
|
||||
entity: "test",
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
@@ -63,8 +64,8 @@ describe("repros", async () => {
|
||||
type: "poly",
|
||||
source: "test",
|
||||
target: "media",
|
||||
config: { mappedBy: "files" }
|
||||
})
|
||||
config: { mappedBy: "files" },
|
||||
}),
|
||||
).resolves.toBeDefined();
|
||||
}
|
||||
|
||||
@@ -75,17 +76,17 @@ describe("repros", async () => {
|
||||
const schema = proto.em(
|
||||
{
|
||||
products: proto.entity("products", {
|
||||
title: proto.text()
|
||||
title: proto.text(),
|
||||
}),
|
||||
product_likes: proto.entity("product_likes", {
|
||||
created_at: proto.date()
|
||||
created_at: proto.date(),
|
||||
}),
|
||||
users: proto.entity("users", {})
|
||||
users: proto.entity("users", {}),
|
||||
},
|
||||
(fns, schema) => {
|
||||
fns.relation(schema.product_likes).manyToOne(schema.products, { inversedBy: "likes" });
|
||||
fns.relation(schema.product_likes).manyToOne(schema.users);
|
||||
}
|
||||
},
|
||||
);
|
||||
const app = createApp({ initialConfig: { data: schema.toJSON() } });
|
||||
await app.build();
|
||||
@@ -96,8 +97,8 @@ describe("repros", async () => {
|
||||
expect(info.relations.listable).toEqual([
|
||||
{
|
||||
entity: "product_likes",
|
||||
ref: "likes"
|
||||
}
|
||||
ref: "likes",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,46 +1,3 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Authenticator, type User, type UserPool } from "../../src/auth";
|
||||
import { cookieConfig } from "../../src/auth/authenticate/Authenticator";
|
||||
import { PasswordStrategy } from "../../src/auth/authenticate/strategies/PasswordStrategy";
|
||||
import * as hash from "../../src/auth/utils/hash";
|
||||
import { Default, parse } from "../../src/core/utils";
|
||||
|
||||
/*class MemoryUserPool implements UserPool {
|
||||
constructor(private users: User[] = []) {}
|
||||
|
||||
async findBy(prop: "id" | "email" | "username", value: string | number) {
|
||||
return this.users.find((user) => user[prop] === value);
|
||||
}
|
||||
|
||||
async create(user: Pick<User, "email" | "password">) {
|
||||
const id = this.users.length + 1;
|
||||
const newUser = { ...user, id, username: user.email };
|
||||
this.users.push(newUser);
|
||||
return newUser;
|
||||
}
|
||||
}*/
|
||||
|
||||
describe("Authenticator", async () => {
|
||||
test("cookie options", async () => {
|
||||
console.log("parsed", parse(cookieConfig, undefined));
|
||||
console.log(Default(cookieConfig, {}));
|
||||
});
|
||||
/*const userpool = new MemoryUserPool([
|
||||
{ id: 1, email: "d", username: "test", password: await hash.sha256("test") },
|
||||
]);
|
||||
|
||||
test("sha256 login", async () => {
|
||||
const auth = new Authenticator(userpool, {
|
||||
password: new PasswordStrategy({
|
||||
hashing: "sha256",
|
||||
}),
|
||||
});
|
||||
|
||||
const { token } = await auth.login("password", { email: "d", password: "test" });
|
||||
expect(token).toBeDefined();
|
||||
|
||||
const { iat, ...decoded } = decodeJwt<any>(token);
|
||||
expect(decoded).toEqual({ id: 1, email: "d", username: "test" });
|
||||
expect(await auth.verify(token)).toBe(true);
|
||||
});*/
|
||||
});
|
||||
describe("Authenticator", async () => {});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Guard } from "../../../src/auth";
|
||||
import { Guard } from "../../../src/auth/authorize/Guard";
|
||||
|
||||
describe("authorize", () => {
|
||||
test("basic", async () => {
|
||||
@@ -7,13 +7,13 @@ describe("authorize", () => {
|
||||
["read", "write"],
|
||||
{
|
||||
admin: {
|
||||
permissions: ["read", "write"]
|
||||
}
|
||||
permissions: ["read", "write"],
|
||||
},
|
||||
},
|
||||
{ enabled: true }
|
||||
{ enabled: true },
|
||||
);
|
||||
const user = {
|
||||
role: "admin"
|
||||
role: "admin",
|
||||
};
|
||||
|
||||
expect(guard.granted("read", user)).toBe(true);
|
||||
@@ -27,21 +27,21 @@ describe("authorize", () => {
|
||||
["read", "write"],
|
||||
{
|
||||
admin: {
|
||||
permissions: ["read", "write"]
|
||||
permissions: ["read", "write"],
|
||||
},
|
||||
guest: {
|
||||
permissions: ["read"],
|
||||
is_default: true
|
||||
}
|
||||
is_default: true,
|
||||
},
|
||||
},
|
||||
{ enabled: true }
|
||||
{ enabled: true },
|
||||
);
|
||||
|
||||
expect(guard.granted("read")).toBe(true);
|
||||
expect(guard.granted("write")).toBe(false);
|
||||
|
||||
const user = {
|
||||
role: "admin"
|
||||
role: "admin",
|
||||
};
|
||||
|
||||
expect(guard.granted("read", user)).toBe(true);
|
||||
@@ -58,12 +58,12 @@ describe("authorize", () => {
|
||||
test("role implicit allow", async () => {
|
||||
const guard = Guard.create(["read", "write"], {
|
||||
admin: {
|
||||
implicit_allow: true
|
||||
}
|
||||
implicit_allow: true,
|
||||
},
|
||||
});
|
||||
|
||||
const user = {
|
||||
role: "admin"
|
||||
role: "admin",
|
||||
};
|
||||
|
||||
expect(guard.granted("read", user)).toBe(true);
|
||||
@@ -74,8 +74,8 @@ describe("authorize", () => {
|
||||
const guard = Guard.create(["read", "write"], {
|
||||
guest: {
|
||||
implicit_allow: true,
|
||||
is_default: true
|
||||
}
|
||||
is_default: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(guard.getUserRole()?.name).toBe("guest");
|
||||
|
||||
@@ -3,14 +3,16 @@ import { OAuthStrategy } from "../../../src/auth/authenticate/strategies";
|
||||
|
||||
const ALL_TESTS = !!process.env.ALL_TESTS;
|
||||
|
||||
// @todo: add mock response
|
||||
describe("OAuthStrategy", async () => {
|
||||
const strategy = new OAuthStrategy({
|
||||
return;
|
||||
/*const strategy = new OAuthStrategy({
|
||||
type: "oidc",
|
||||
client: {
|
||||
client_id: process.env.OAUTH_CLIENT_ID,
|
||||
client_secret: process.env.OAUTH_CLIENT_SECRET
|
||||
client_id: process.env.OAUTH_CLIENT_ID!,
|
||||
client_secret: process.env.OAUTH_CLIENT_SECRET!,
|
||||
},
|
||||
name: "google"
|
||||
name: "google",
|
||||
});
|
||||
const state = "---";
|
||||
const redirect_uri = "http://localhost:3000/auth/google/callback";
|
||||
@@ -19,28 +21,29 @@ describe("OAuthStrategy", async () => {
|
||||
const config = await strategy.getConfig();
|
||||
console.log("config", JSON.stringify(config, null, 2));
|
||||
|
||||
const request = await strategy.request({
|
||||
redirect_uri,
|
||||
state
|
||||
});
|
||||
|
||||
const server = Bun.serve({
|
||||
fetch: async (req) => {
|
||||
console.log("req", req.method, req.url);
|
||||
const url = new URL(req.url);
|
||||
if (url.pathname === "/auth/google/callback") {
|
||||
console.log("req", req);
|
||||
const user = await strategy.callback(url, {
|
||||
redirect_uri,
|
||||
state
|
||||
state,
|
||||
});
|
||||
|
||||
console.log("---user", user);
|
||||
}
|
||||
return new Response("Bun!");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const request = await strategy.request({
|
||||
redirect_uri,
|
||||
state,
|
||||
});
|
||||
console.log("request", request);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100000));
|
||||
});
|
||||
});*/
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ class ReturnEvent extends Event<{ foo: string }, string> {
|
||||
}
|
||||
|
||||
return this.clone({
|
||||
foo: [this.params.foo, value].join("-")
|
||||
foo: [this.params.foo, value].join("-"),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ describe("EventManager", async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
delayed();
|
||||
},
|
||||
"sync"
|
||||
"sync",
|
||||
);
|
||||
|
||||
// don't allow unknown
|
||||
@@ -70,6 +70,9 @@ describe("EventManager", async () => {
|
||||
new SpecialEvent({ foo: "bar" });
|
||||
new InformationalEvent();
|
||||
|
||||
// execute asyncs
|
||||
await emgr.executeAsyncs();
|
||||
|
||||
expect(call).toHaveBeenCalledTimes(2);
|
||||
expect(delayed).toHaveBeenCalled();
|
||||
});
|
||||
@@ -80,15 +83,11 @@ describe("EventManager", async () => {
|
||||
call();
|
||||
return Promise.all(p);
|
||||
};
|
||||
const emgr = new EventManager(
|
||||
{ InformationalEvent },
|
||||
{
|
||||
asyncExecutor
|
||||
}
|
||||
);
|
||||
const emgr = new EventManager({ InformationalEvent });
|
||||
|
||||
emgr.onEvent(InformationalEvent, async () => {});
|
||||
await emgr.emit(new InformationalEvent());
|
||||
await emgr.executeAsyncs(asyncExecutor);
|
||||
expect(call).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -98,8 +97,8 @@ describe("EventManager", async () => {
|
||||
const emgr = new EventManager(
|
||||
{ ReturnEvent, InformationalEvent },
|
||||
{
|
||||
onInvalidReturn
|
||||
}
|
||||
onInvalidReturn,
|
||||
},
|
||||
);
|
||||
|
||||
// @ts-expect-error InformationalEvent has no return value
|
||||
@@ -125,6 +124,9 @@ describe("EventManager", async () => {
|
||||
const e2 = await emgr.emit(new ReturnEvent({ foo: "bar" }));
|
||||
expect(e2.returned).toBe(true);
|
||||
expect(e2.params.foo).toBe("bar-1-0");
|
||||
|
||||
await emgr.executeAsyncs();
|
||||
|
||||
expect(onInvalidReturn).toHaveBeenCalled();
|
||||
expect(asyncEventCallback).toHaveBeenCalled();
|
||||
});
|
||||
@@ -140,7 +142,7 @@ describe("EventManager", async () => {
|
||||
expect(slug).toBe("informational-event");
|
||||
call();
|
||||
},
|
||||
{ mode: "sync", once: true }
|
||||
{ mode: "sync", once: true },
|
||||
);
|
||||
|
||||
expect(emgr.getListeners().length).toBe(1);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { TObject, TString } from "@sinclair/typebox";
|
||||
import { Registry } from "../../src/core/registry/Registry";
|
||||
import { type TSchema, Type } from "../../src/core/utils";
|
||||
import { Registry } from "core/registry/Registry";
|
||||
import { s } from "core/utils/schema";
|
||||
|
||||
type Constructor<T> = new (...args: any[]) => T;
|
||||
|
||||
@@ -12,7 +11,7 @@ class What {
|
||||
return null;
|
||||
}
|
||||
getType() {
|
||||
return Type.Object({ type: Type.String() });
|
||||
return s.object({ type: s.string() });
|
||||
}
|
||||
}
|
||||
class What2 extends What {}
|
||||
@@ -20,7 +19,7 @@ class NotAllowed {}
|
||||
|
||||
type Test1 = {
|
||||
cls: new (...args: any[]) => What;
|
||||
schema: TObject<{ type: TString }>;
|
||||
schema: s.ObjectSchema<{ type: s.StringSchema }>;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
@@ -29,40 +28,40 @@ describe("Registry", () => {
|
||||
const registry = new Registry<Test1>().set({
|
||||
first: {
|
||||
cls: What,
|
||||
schema: Type.Object({ type: Type.String(), what: Type.String() }),
|
||||
enabled: true
|
||||
}
|
||||
schema: s.object({ type: s.string(), what: s.string() }),
|
||||
enabled: true,
|
||||
},
|
||||
} satisfies Record<string, Test1>);
|
||||
|
||||
const item = registry.get("first");
|
||||
expect(item).toBeDefined();
|
||||
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", {
|
||||
cls: What2,
|
||||
schema: second,
|
||||
enabled: true
|
||||
enabled: true,
|
||||
});
|
||||
// @ts-ignore
|
||||
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", {
|
||||
// @ts-expect-error
|
||||
cls: NotAllowed,
|
||||
schema: third,
|
||||
enabled: true
|
||||
enabled: true,
|
||||
});
|
||||
// @ts-ignore
|
||||
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", {
|
||||
cls: What,
|
||||
// @ts-expect-error
|
||||
schema: fourth,
|
||||
enabled: true
|
||||
enabled: true,
|
||||
});
|
||||
// @ts-ignore
|
||||
expect(registry.get("fourth").schema).toEqual(fourth);
|
||||
@@ -75,13 +74,15 @@ describe("Registry", () => {
|
||||
return {
|
||||
cls: a,
|
||||
schema: a.prototype.getType(),
|
||||
enabled: true
|
||||
enabled: true,
|
||||
};
|
||||
});
|
||||
|
||||
registry.register("what2", What2);
|
||||
expect(registry.get("what2")).toBeDefined();
|
||||
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);
|
||||
});*/
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { checksum, hash } from "../../src/core/utils";
|
||||
describe("crypto", async () => {
|
||||
test("sha256", async () => {
|
||||
expect(await hash.sha256("test")).toBe(
|
||||
"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
|
||||
"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
|
||||
);
|
||||
});
|
||||
test("sha1", async () => {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { env, is_toggled } from "core/env";
|
||||
|
||||
describe("env", () => {
|
||||
test("is_toggled", () => {
|
||||
expect(is_toggled("true")).toBe(true);
|
||||
expect(is_toggled("1")).toBe(true);
|
||||
expect(is_toggled("false")).toBe(false);
|
||||
expect(is_toggled("0")).toBe(false);
|
||||
expect(is_toggled(true)).toBe(true);
|
||||
expect(is_toggled(false)).toBe(false);
|
||||
expect(is_toggled(undefined)).toBe(false);
|
||||
expect(is_toggled(null)).toBe(false);
|
||||
expect(is_toggled(1)).toBe(true);
|
||||
expect(is_toggled(0)).toBe(false);
|
||||
expect(is_toggled("anything else")).toBe(false);
|
||||
expect(is_toggled(undefined, true)).toBe(true);
|
||||
});
|
||||
|
||||
test("env()", () => {
|
||||
expect(env("cli_log_level", undefined, { source: {} })).toBeUndefined();
|
||||
expect(env("cli_log_level", undefined, { source: { BKND_CLI_LOG_LEVEL: "log" } })).toBe(
|
||||
"log" as any,
|
||||
);
|
||||
expect(env("cli_log_level", undefined, { source: { BKND_CLI_LOG_LEVEL: "LOG" } })).toBe(
|
||||
"log" as any,
|
||||
);
|
||||
expect(
|
||||
env("cli_log_level", undefined, { source: { BKND_CLI_LOG_LEVEL: "asdf" } }),
|
||||
).toBeUndefined();
|
||||
|
||||
expect(env("modules_debug", undefined, { source: {} })).toBeFalse();
|
||||
expect(env("modules_debug", undefined, { source: { BKND_MODULES_DEBUG: "1" } })).toBeTrue();
|
||||
expect(env("modules_debug", undefined, { source: { BKND_MODULES_DEBUG: "0" } })).toBeFalse();
|
||||
});
|
||||
});
|
||||
@@ -1,15 +1,15 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { SchemaObject } from "../../../src/core";
|
||||
import { Type } from "../../../src/core/utils";
|
||||
import { s } from "core/utils/schema";
|
||||
import { SchemaObject } from "core/object/SchemaObject";
|
||||
|
||||
describe("SchemaObject", async () => {
|
||||
test("basic", async () => {
|
||||
const m = new SchemaObject(
|
||||
Type.Object({ a: Type.String({ default: "b" }) }),
|
||||
s.strictObject({ a: s.string({ default: "b" }) }),
|
||||
{ a: "test" },
|
||||
{
|
||||
forceParse: true
|
||||
}
|
||||
forceParse: true,
|
||||
},
|
||||
);
|
||||
|
||||
expect(m.get()).toEqual({ a: "test" });
|
||||
@@ -23,28 +23,28 @@ describe("SchemaObject", async () => {
|
||||
|
||||
test("patch", async () => {
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
s: Type.Object(
|
||||
s.strictObject({
|
||||
s: s.strictObject(
|
||||
{
|
||||
a: Type.String({ default: "b" }),
|
||||
b: Type.Object(
|
||||
a: s.string({ default: "b" }),
|
||||
b: s.strictObject(
|
||||
{
|
||||
c: Type.String({ default: "d" }),
|
||||
e: Type.String({ default: "f" })
|
||||
c: s.string({ default: "d" }),
|
||||
e: s.string({ default: "f" }),
|
||||
},
|
||||
{ default: {} }
|
||||
)
|
||||
{ default: {} },
|
||||
),
|
||||
},
|
||||
{ default: {}, additionalProperties: false }
|
||||
)
|
||||
})
|
||||
{ default: {} },
|
||||
),
|
||||
}),
|
||||
);
|
||||
expect(m.get()).toEqual({ s: { a: "b", b: { c: "d", e: "f" } } });
|
||||
|
||||
await m.patch("s.a", "c");
|
||||
|
||||
// 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
|
||||
expect(() => m.patch("s.a", 1)).toThrow();
|
||||
|
||||
@@ -58,9 +58,9 @@ describe("SchemaObject", async () => {
|
||||
|
||||
test("patch array", async () => {
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] })
|
||||
})
|
||||
s.strictObject({
|
||||
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
||||
}),
|
||||
);
|
||||
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
|
||||
|
||||
@@ -75,20 +75,20 @@ describe("SchemaObject", async () => {
|
||||
|
||||
test("remove", async () => {
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
s: Type.Object(
|
||||
s.object({
|
||||
s: s.object(
|
||||
{
|
||||
a: Type.String({ default: "b" }),
|
||||
b: Type.Object(
|
||||
a: s.string({ default: "b" }),
|
||||
b: s.object(
|
||||
{
|
||||
c: Type.String({ default: "d" })
|
||||
c: s.string({ default: "d" }),
|
||||
},
|
||||
{ default: {} }
|
||||
)
|
||||
{ default: {} },
|
||||
),
|
||||
},
|
||||
{ default: {} }
|
||||
)
|
||||
})
|
||||
{ default: {} },
|
||||
),
|
||||
}),
|
||||
);
|
||||
expect(m.get()).toEqual({ s: { a: "b", b: { c: "d" } } });
|
||||
|
||||
@@ -107,9 +107,9 @@ describe("SchemaObject", async () => {
|
||||
|
||||
test("set", async () => {
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] })
|
||||
})
|
||||
s.strictObject({
|
||||
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
||||
}),
|
||||
);
|
||||
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
|
||||
|
||||
@@ -124,8 +124,8 @@ describe("SchemaObject", async () => {
|
||||
let called = false;
|
||||
let result: any;
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] })
|
||||
s.strictObject({
|
||||
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
||||
}),
|
||||
undefined,
|
||||
{
|
||||
@@ -133,8 +133,8 @@ describe("SchemaObject", async () => {
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
called = true;
|
||||
result = config;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await m.set({ methods: ["GET", "POST"] });
|
||||
@@ -145,8 +145,8 @@ describe("SchemaObject", async () => {
|
||||
test("listener: onBeforeUpdate", async () => {
|
||||
let called = false;
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] })
|
||||
s.strictObject({
|
||||
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
||||
}),
|
||||
undefined,
|
||||
{
|
||||
@@ -155,8 +155,8 @@ describe("SchemaObject", async () => {
|
||||
called = true;
|
||||
to.methods.push("OPTIONS");
|
||||
return to;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const result = await m.set({ methods: ["GET", "POST"] });
|
||||
@@ -167,8 +167,8 @@ describe("SchemaObject", async () => {
|
||||
});
|
||||
|
||||
test("throwIfRestricted", async () => {
|
||||
const m = new SchemaObject(Type.Object({}), undefined, {
|
||||
restrictPaths: ["a.b"]
|
||||
const m = new SchemaObject(s.strictObject({}), undefined, {
|
||||
restrictPaths: ["a.b"],
|
||||
});
|
||||
|
||||
expect(() => m.throwIfRestricted("a.b")).toThrow();
|
||||
@@ -179,24 +179,24 @@ describe("SchemaObject", async () => {
|
||||
|
||||
test("restriction bypass", async () => {
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
s: Type.Object(
|
||||
s.strictObject({
|
||||
s: s.strictObject(
|
||||
{
|
||||
a: Type.String({ default: "b" }),
|
||||
b: Type.Object(
|
||||
a: s.string({ default: "b" }),
|
||||
b: s.strictObject(
|
||||
{
|
||||
c: Type.String({ default: "d" })
|
||||
c: s.string({ default: "d" }),
|
||||
},
|
||||
{ default: {} }
|
||||
)
|
||||
{ default: {} },
|
||||
),
|
||||
},
|
||||
{ default: {} }
|
||||
)
|
||||
{ default: {} },
|
||||
),
|
||||
}),
|
||||
undefined,
|
||||
{
|
||||
restrictPaths: ["s.b"]
|
||||
}
|
||||
restrictPaths: ["s.b"],
|
||||
},
|
||||
);
|
||||
|
||||
expect(m.patch("s.b.c", "e")).rejects.toThrow();
|
||||
@@ -205,7 +205,21 @@ describe("SchemaObject", async () => {
|
||||
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(
|
||||
{},
|
||||
@@ -217,33 +231,33 @@ describe("SchemaObject", async () => {
|
||||
additionalProperties: Type.Object({
|
||||
type: Type.String(),
|
||||
config: Type.Optional(
|
||||
Type.Object({}, { additionalProperties: Type.String() })
|
||||
)
|
||||
})
|
||||
}
|
||||
Type.Object({}, { additionalProperties: Type.String() }),
|
||||
),
|
||||
}),
|
||||
},
|
||||
),
|
||||
config: Type.Optional(Type.Object({}, { additionalProperties: Type.String() }))
|
||||
})
|
||||
}
|
||||
)
|
||||
config: Type.Optional(Type.Object({}, { additionalProperties: Type.String() })),
|
||||
}),
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
additionalProperties: false
|
||||
}
|
||||
);
|
||||
additionalProperties: false,
|
||||
},
|
||||
); */
|
||||
test("patch safe object, overwrite", async () => {
|
||||
const data = {
|
||||
entities: {
|
||||
some: {
|
||||
fields: {
|
||||
a: { type: "string", config: { some: "thing" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
a: { type: "string", config: { some: "thing" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const m = new SchemaObject(dataEntitiesSchema, data, {
|
||||
forceParse: true,
|
||||
overwritePaths: [/^entities\..*\.fields\..*\.config/]
|
||||
overwritePaths: [/^entities\..*\.fields\..*\.config/],
|
||||
});
|
||||
|
||||
await m.patch("entities.some.fields.a", { type: "string", config: { another: "one" } });
|
||||
@@ -252,10 +266,10 @@ describe("SchemaObject", async () => {
|
||||
entities: {
|
||||
some: {
|
||||
fields: {
|
||||
a: { type: "string", config: { another: "one" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
a: { type: "string", config: { another: "one" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -265,22 +279,22 @@ describe("SchemaObject", async () => {
|
||||
users: {
|
||||
fields: {
|
||||
email: { type: "string" },
|
||||
password: { type: "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
password: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const m = new SchemaObject(dataEntitiesSchema, data, {
|
||||
forceParse: true,
|
||||
overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/]
|
||||
overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/],
|
||||
});
|
||||
|
||||
await m.patch("entities.test", {
|
||||
fields: {
|
||||
content: {
|
||||
type: "text"
|
||||
}
|
||||
}
|
||||
type: "text",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(m.get()).toEqual({
|
||||
@@ -288,17 +302,17 @@ describe("SchemaObject", async () => {
|
||||
users: {
|
||||
fields: {
|
||||
email: { type: "string" },
|
||||
password: { type: "string" }
|
||||
}
|
||||
password: { type: "string" },
|
||||
},
|
||||
},
|
||||
test: {
|
||||
fields: {
|
||||
content: {
|
||||
type: "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
type: "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -308,14 +322,14 @@ describe("SchemaObject", async () => {
|
||||
users: {
|
||||
fields: {
|
||||
email: { type: "string" },
|
||||
password: { type: "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
password: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const m = new SchemaObject(dataEntitiesSchema, data, {
|
||||
forceParse: true,
|
||||
overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/]
|
||||
overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/],
|
||||
});
|
||||
|
||||
expect(m.patch("desc", "entities.users.config.sort_dir")).rejects.toThrow();
|
||||
@@ -323,13 +337,13 @@ describe("SchemaObject", async () => {
|
||||
await m.patch("entities.test", {
|
||||
fields: {
|
||||
content: {
|
||||
type: "text"
|
||||
}
|
||||
}
|
||||
type: "text",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await m.patch("entities.users.config", {
|
||||
sort_dir: "desc"
|
||||
sort_dir: "desc",
|
||||
});
|
||||
|
||||
expect(m.get()).toEqual({
|
||||
@@ -337,20 +351,20 @@ describe("SchemaObject", async () => {
|
||||
users: {
|
||||
fields: {
|
||||
email: { type: "string" },
|
||||
password: { type: "string" }
|
||||
password: { type: "string" },
|
||||
},
|
||||
config: {
|
||||
sort_dir: "desc"
|
||||
}
|
||||
sort_dir: "desc",
|
||||
},
|
||||
},
|
||||
test: {
|
||||
fields: {
|
||||
content: {
|
||||
type: "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
type: "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,8 +13,8 @@ describe("diff", () => {
|
||||
t: "a",
|
||||
p: ["b"],
|
||||
o: undefined,
|
||||
n: 2
|
||||
}
|
||||
n: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -35,8 +35,8 @@ describe("diff", () => {
|
||||
t: "r",
|
||||
p: ["b"],
|
||||
o: 2,
|
||||
n: undefined
|
||||
}
|
||||
n: undefined,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -57,8 +57,8 @@ describe("diff", () => {
|
||||
t: "e",
|
||||
p: ["a"],
|
||||
o: 1,
|
||||
n: 2
|
||||
}
|
||||
n: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -79,8 +79,8 @@ describe("diff", () => {
|
||||
t: "e",
|
||||
p: ["a", "b"],
|
||||
o: 1,
|
||||
n: 2
|
||||
}
|
||||
n: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -101,14 +101,14 @@ describe("diff", () => {
|
||||
t: "e",
|
||||
p: ["a", 1],
|
||||
o: 2,
|
||||
n: 4
|
||||
n: 4,
|
||||
},
|
||||
{
|
||||
t: "a",
|
||||
p: ["a", 3],
|
||||
o: undefined,
|
||||
n: 5
|
||||
}
|
||||
n: 5,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -129,20 +129,20 @@ describe("diff", () => {
|
||||
t: "a",
|
||||
p: ["a", 0],
|
||||
o: undefined,
|
||||
n: 1
|
||||
n: 1,
|
||||
},
|
||||
{
|
||||
t: "a",
|
||||
p: ["a", 1],
|
||||
o: undefined,
|
||||
n: 2
|
||||
n: 2,
|
||||
},
|
||||
{
|
||||
t: "a",
|
||||
p: ["a", 2],
|
||||
o: undefined,
|
||||
n: 3
|
||||
}
|
||||
n: 3,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -163,14 +163,14 @@ describe("diff", () => {
|
||||
t: "e",
|
||||
p: ["a", 1],
|
||||
o: 2,
|
||||
n: 3
|
||||
n: 3,
|
||||
},
|
||||
{
|
||||
t: "r",
|
||||
p: ["a", 2],
|
||||
o: 3,
|
||||
n: undefined
|
||||
}
|
||||
n: undefined,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -183,14 +183,14 @@ describe("diff", () => {
|
||||
it("should handle complex nested changes", () => {
|
||||
const oldObj = {
|
||||
a: {
|
||||
b: [1, 2, { c: 3 }]
|
||||
}
|
||||
b: [1, 2, { c: 3 }],
|
||||
},
|
||||
};
|
||||
|
||||
const newObj = {
|
||||
a: {
|
||||
b: [1, 2, { c: 4 }, 5]
|
||||
}
|
||||
b: [1, 2, { c: 4 }, 5],
|
||||
},
|
||||
};
|
||||
|
||||
const diffs = diff(oldObj, newObj);
|
||||
@@ -200,14 +200,14 @@ describe("diff", () => {
|
||||
t: "e",
|
||||
p: ["a", "b", 2, "c"],
|
||||
o: 3,
|
||||
n: 4
|
||||
n: 4,
|
||||
},
|
||||
{
|
||||
t: "a",
|
||||
p: ["a", "b", 3],
|
||||
o: undefined,
|
||||
n: 5
|
||||
}
|
||||
n: 5,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -228,14 +228,14 @@ describe("diff", () => {
|
||||
t: "e",
|
||||
p: ["a"],
|
||||
o: undefined,
|
||||
n: null
|
||||
n: null,
|
||||
},
|
||||
{
|
||||
t: "e",
|
||||
p: ["b"],
|
||||
o: null,
|
||||
n: undefined
|
||||
}
|
||||
n: undefined,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -256,8 +256,8 @@ describe("diff", () => {
|
||||
t: "e",
|
||||
p: ["a"],
|
||||
o: 1,
|
||||
n: "1"
|
||||
}
|
||||
n: "1",
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -278,14 +278,14 @@ describe("diff", () => {
|
||||
t: "r",
|
||||
p: ["b"],
|
||||
o: 2,
|
||||
n: undefined
|
||||
n: undefined,
|
||||
},
|
||||
{
|
||||
t: "a",
|
||||
p: ["c"],
|
||||
o: undefined,
|
||||
n: 3
|
||||
}
|
||||
n: 3,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -306,8 +306,8 @@ describe("diff", () => {
|
||||
t: "e",
|
||||
p: ["a"],
|
||||
o: [1, 2, 3],
|
||||
n: { b: 4 }
|
||||
}
|
||||
n: { b: 4 },
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -328,8 +328,8 @@ describe("diff", () => {
|
||||
t: "e",
|
||||
p: ["a"],
|
||||
o: { b: 1 },
|
||||
n: 2
|
||||
}
|
||||
n: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -350,14 +350,14 @@ describe("diff", () => {
|
||||
t: "r",
|
||||
p: ["a"],
|
||||
o: 1,
|
||||
n: undefined
|
||||
n: undefined,
|
||||
},
|
||||
{
|
||||
t: "a",
|
||||
p: ["b"],
|
||||
o: undefined,
|
||||
n: 2
|
||||
}
|
||||
n: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -408,8 +408,8 @@ describe("diff", () => {
|
||||
t: "a",
|
||||
p: ["a"],
|
||||
o: undefined,
|
||||
n: 1
|
||||
}
|
||||
n: 1,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -430,8 +430,8 @@ describe("diff", () => {
|
||||
t: "r",
|
||||
p: ["a"],
|
||||
o: 1,
|
||||
n: undefined
|
||||
}
|
||||
n: undefined,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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", () => {
|
||||
const q: ObjectQuery = { name: "Michael" };
|
||||
@@ -9,7 +9,7 @@ describe("object-query", () => {
|
||||
|
||||
test("validates", async () => {
|
||||
const converted = convert({
|
||||
name: { $eq: "ch" }
|
||||
name: { $eq: "ch" },
|
||||
});
|
||||
validate(converted, { name: "Michael" });
|
||||
});
|
||||
@@ -31,7 +31,7 @@ describe("object-query", () => {
|
||||
[{ val: { $notnull: 1 } }, { val: null }, false],
|
||||
[{ val: { $regex: ".*" } }, { val: "test" }, true],
|
||||
[{ val: { $regex: /^t.*/ } }, { val: "test" }, true],
|
||||
[{ val: { $regex: /^b.*/ } }, { val: "test" }, false]
|
||||
[{ val: { $regex: /^b.*/ } }, { val: "test" }, false],
|
||||
];
|
||||
|
||||
for (const [query, object, expected] of tests) {
|
||||
@@ -55,10 +55,10 @@ describe("object-query", () => {
|
||||
[
|
||||
{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } },
|
||||
{ val1: "foo", val2: "bar" },
|
||||
true
|
||||
true,
|
||||
],
|
||||
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, { val1: 1 }, true],
|
||||
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, { val1: 3 }, false]
|
||||
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, { val1: 3 }, false],
|
||||
];
|
||||
|
||||
for (const [query, object, expected] of tests) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Perf, isBlob, ucFirst } from "../../src/core/utils";
|
||||
import { Perf, ucFirst } from "../../src/core/utils";
|
||||
import * as utils from "../../src/core/utils";
|
||||
import { assetsPath } from "../helper";
|
||||
|
||||
async function wait(ms: number) {
|
||||
return new Promise((resolve) => {
|
||||
@@ -16,7 +17,7 @@ describe("Core Utils", async () => {
|
||||
expect(result).toEqual([
|
||||
{ key: "a", value: 1 },
|
||||
{ key: "b", value: 2 },
|
||||
{ key: "c", value: 3 }
|
||||
{ key: "c", value: 3 },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -51,7 +52,7 @@ describe("Core Utils", async () => {
|
||||
const obj = utils.headersToObject(headers);
|
||||
expect(obj).toEqual({
|
||||
"content-type": "application/json",
|
||||
authorization: "Bearer 123"
|
||||
authorization: "Bearer 123",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -75,57 +76,6 @@ describe("Core Utils", async () => {
|
||||
const result3 = utils.encodeSearch(obj3, { encode: true });
|
||||
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 () => {
|
||||
@@ -166,7 +116,7 @@ describe("Core Utils", async () => {
|
||||
[{ a: 1, b: 2, c: 3 }, ["b"], { a: 1, c: 3 }],
|
||||
[{ a: 1, b: 2, c: 3 }, ["c"], { a: 1, b: 2 }],
|
||||
[{ a: 1, b: 2, c: 3 }, ["a", "b"], { c: 3 }],
|
||||
[{ a: 1, b: 2, c: 3 }, ["a", "b", "c"], {}]
|
||||
[{ a: 1, b: 2, c: 3 }, ["a", "b", "c"], {}],
|
||||
] as [object, string[], object][];
|
||||
|
||||
for (const [obj, keys, expected] of objects) {
|
||||
@@ -197,9 +147,9 @@ describe("Core Utils", async () => {
|
||||
new Map([["a", 1]]),
|
||||
new Map([
|
||||
["a", 1],
|
||||
["b", 2]
|
||||
["b", 2],
|
||||
]),
|
||||
false
|
||||
false,
|
||||
],
|
||||
[{ a: 1 }, { a: 1 }, true],
|
||||
[{ a: 1 }, { a: 2 }, false],
|
||||
@@ -220,7 +170,7 @@ describe("Core Utils", async () => {
|
||||
[[1, 2, 3], [1, 2, 3, 4], false],
|
||||
[[{ a: 1 }], [{ a: 1 }], true],
|
||||
[[{ a: 1 }], [{ a: 2 }], false],
|
||||
[[{ a: 1 }], [{ b: 1 }], false]
|
||||
[[{ a: 1 }], [{ b: 1 }], false],
|
||||
] as [any, any, boolean][];
|
||||
|
||||
for (const [a, b, expected] of objects) {
|
||||
@@ -236,7 +186,7 @@ describe("Core Utils", async () => {
|
||||
[{ a: { b: 1 } }, "a.b", 1],
|
||||
[{ a: { b: 1 } }, "a.b.c", null, null],
|
||||
[{ a: { b: 1 } }, "a.b.c", 1, 1],
|
||||
[[[1]], "0.0", 1]
|
||||
[[[1]], "0.0", 1],
|
||||
] as [object, string, any, any][];
|
||||
|
||||
for (const [obj, path, expected, defaultValue] of tests) {
|
||||
@@ -245,4 +195,84 @@ 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", () => {
|
||||
test.only("formats local time", () => {
|
||||
expect(utils.datetimeStringUTC("2025-02-21T16:48:25.841Z")).toBe("2025-02-21 16:48:25");
|
||||
console.log(utils.datetimeStringUTC(new Date()));
|
||||
console.log(utils.datetimeStringUTC());
|
||||
console.log(new Date());
|
||||
console.log("timezone", Intl.DateTimeFormat().resolvedOptions().timeZone);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
|
||||
import { Guard } from "../../src/auth";
|
||||
import { parse } from "../../src/core/utils";
|
||||
import {
|
||||
Entity,
|
||||
type EntityData,
|
||||
EntityManager,
|
||||
ManyToOneRelation,
|
||||
type MutatorResponse,
|
||||
type RepositoryResponse,
|
||||
TextField
|
||||
} from "../../src/data";
|
||||
import { Guard } from "../../src/auth/authorize/Guard";
|
||||
import { parse } from "core/utils/schema";
|
||||
|
||||
import { DataController } from "../../src/data/api/DataController";
|
||||
import { dataConfigSchema } from "../../src/data/data-schema";
|
||||
import { disableConsoleLog, enableConsoleLog, getDummyConnection } from "../helper";
|
||||
import type { RepositoryResultJSON } from "data/entities/query/RepositoryResult";
|
||||
import type { MutatorResultJSON } from "data/entities/mutation/MutatorResult";
|
||||
import { Entity, EntityManager, type EntityData } from "data/entities";
|
||||
import { TextField } from "data/fields";
|
||||
import { ManyToOneRelation } from "data/relations";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
beforeAll(() => disableConsoleLog(["log", "warn"]));
|
||||
@@ -21,60 +18,14 @@ afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||
|
||||
const dataConfig = parse(dataConfigSchema, {});
|
||||
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 () => {
|
||||
const users = new Entity("users", [
|
||||
new TextField("name", { required: true }),
|
||||
new TextField("bio")
|
||||
new TextField("bio"),
|
||||
]);
|
||||
const posts = new Entity("posts", [new TextField("content")]);
|
||||
const em = new EntityManager([users, posts], dummyConnection, [
|
||||
new ManyToOneRelation(posts, users)
|
||||
new ManyToOneRelation(posts, users),
|
||||
]);
|
||||
|
||||
await em.schema().sync({ force: true });
|
||||
@@ -83,12 +34,12 @@ describe("[data] DataController", async () => {
|
||||
users: [
|
||||
{ name: "foo", bio: "bar" },
|
||||
{ name: "bar", bio: null },
|
||||
{ name: "baz", bio: "!!!" }
|
||||
{ name: "baz", bio: "!!!" },
|
||||
],
|
||||
posts: [
|
||||
{ content: "post 1", users_id: 1 },
|
||||
{ content: "post 2", users_id: 2 }
|
||||
]
|
||||
{ content: "post 2", users_id: 2 },
|
||||
],
|
||||
};
|
||||
|
||||
const ctx: any = { em, guard: new Guard() };
|
||||
@@ -118,10 +69,9 @@ describe("[data] DataController", async () => {
|
||||
for await (const _user of fixtures.users) {
|
||||
const res = await app.request("/entity/users", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(_user)
|
||||
body: JSON.stringify(_user),
|
||||
});
|
||||
//console.log("res", { _user }, res);
|
||||
const result = (await res.json()) as MutatorResponse;
|
||||
const result = (await res.json()) as MutatorResultJSON;
|
||||
const { id, ...data } = result.data as any;
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
@@ -133,9 +83,9 @@ describe("[data] DataController", async () => {
|
||||
for await (const _post of fixtures.posts) {
|
||||
const res = await app.request("/entity/posts", {
|
||||
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;
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
@@ -146,52 +96,51 @@ describe("[data] DataController", async () => {
|
||||
|
||||
test("/:entity (read many)", async () => {
|
||||
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.count).toBe(3);
|
||||
//expect(data.meta.total).toBe(3);
|
||||
//expect(data.meta.count).toBe(3);
|
||||
expect(data.meta.items).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 () => {
|
||||
const res = await app.request("/entity/users/query", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
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.count).toBe(1);
|
||||
//expect(data.meta.total).toBe(3);
|
||||
//expect(data.meta.count).toBe(1);
|
||||
expect(data.meta.items).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 () => {
|
||||
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.count).toBe(3);
|
||||
//expect(data.meta.total).toBe(3);
|
||||
//expect(data.meta.count).toBe(3);
|
||||
expect(data.meta.items).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 () => {
|
||||
const res = await app.request("/entity/users/3");
|
||||
const data = (await res.json()) as RepositoryResponse<EntityData>;
|
||||
console.log("data", data);
|
||||
const data = (await res.json()) as RepositoryResultJSON<EntityData>;
|
||||
|
||||
expect(data.meta.total).toBe(3);
|
||||
expect(data.meta.count).toBe(1);
|
||||
//expect(data.meta.total).toBe(3);
|
||||
//expect(data.meta.count).toBe(1);
|
||||
expect(data.meta.items).toBe(1);
|
||||
expect(data.data).toEqual({ id: 3, ...fixtures.users[2] });
|
||||
});
|
||||
@@ -199,9 +148,9 @@ describe("[data] DataController", async () => {
|
||||
test("/:entity (update one)", async () => {
|
||||
const res = await app.request("/entity/users/3", {
|
||||
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(data as any).toEqual({ id: 3, ...fixtures.users[2], name: "new name" });
|
||||
@@ -209,27 +158,26 @@ describe("[data] DataController", async () => {
|
||||
|
||||
test("/:entity/:id/:reference (read references)", async () => {
|
||||
const res = await app.request("/entity/users/1/posts");
|
||||
const data = (await res.json()) as RepositoryResponse;
|
||||
console.log("data", data);
|
||||
const data = (await res.json()) as RepositoryResultJSON;
|
||||
|
||||
expect(data.meta.total).toBe(2);
|
||||
expect(data.meta.count).toBe(1);
|
||||
//expect(data.meta.total).toBe(2);
|
||||
//expect(data.meta.count).toBe(1);
|
||||
expect(data.meta.items).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 () => {
|
||||
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] });
|
||||
|
||||
// verify
|
||||
const res2 = await app.request("/entity/posts");
|
||||
const data2 = (await res2.json()) as RepositoryResponse;
|
||||
expect(data2.meta.total).toBe(1);
|
||||
const data2 = (await res2.json()) as RepositoryResultJSON;
|
||||
//expect(data2.meta.total).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Value, _jsonp } from "../../src/core/utils";
|
||||
import { type RepoQuery, WhereBuilder, type WhereQuery, querySchema } from "../../src/data";
|
||||
import type { RepoQueryIn } from "../../src/data/server/data-query-impl";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const decode = (input: RepoQueryIn, expected: RepoQuery) => {
|
||||
const result = Value.Decode(querySchema, input);
|
||||
expect(result).toEqual(expected);
|
||||
};
|
||||
|
||||
describe("data-query-impl", () => {
|
||||
function qb() {
|
||||
const c = getDummyConnection();
|
||||
const kysely = c.dummyConnection.kysely;
|
||||
return kysely.selectFrom("t").selectAll();
|
||||
}
|
||||
function compile(q: WhereQuery) {
|
||||
const { sql, parameters } = WhereBuilder.addClause(qb(), q).compile();
|
||||
return { sql, parameters };
|
||||
}
|
||||
|
||||
test("single validation", () => {
|
||||
const tests: [WhereQuery, string, any[]][] = [
|
||||
[{ name: "Michael", age: 40 }, '("name" = ? and "age" = ?)', ["Michael", 40]],
|
||||
[{ name: { $eq: "Michael" } }, '"name" = ?', ["Michael"]],
|
||||
[{ int: { $between: [1, 2] } }, '"int" between ? and ?', [1, 2]],
|
||||
[{ val: { $isnull: 1 } }, '"val" is null', []],
|
||||
[{ val: { $isnull: true } }, '"val" is null', []],
|
||||
[{ val: { $isnull: 0 } }, '"val" is not null', []],
|
||||
[{ val: { $isnull: false } }, '"val" is not null', []],
|
||||
[{ val: { $like: "what" } }, '"val" like ?', ["what"]],
|
||||
[{ val: { $like: "w*t" } }, '"val" like ?', ["w%t"]]
|
||||
];
|
||||
|
||||
for (const [query, expectedSql, expectedParams] of tests) {
|
||||
const { sql, parameters } = compile(query);
|
||||
expect(sql).toContain(`select * from "t" where ${expectedSql}`);
|
||||
expect(parameters).toEqual(expectedParams);
|
||||
}
|
||||
});
|
||||
|
||||
test("multiple validations", () => {
|
||||
const tests: [WhereQuery, string, any[]][] = [
|
||||
// multiple constraints per property
|
||||
[{ val: { $lt: 10, $gte: 3 } }, '("val" < ? and "val" >= ?)', [10, 3]],
|
||||
[{ val: { $lt: 10, $gte: 3 } }, '("val" < ? and "val" >= ?)', [10, 3]],
|
||||
[{ val: { $lt: 10, $gte: 3 } }, '("val" < ? and "val" >= ?)', [10, 3]],
|
||||
|
||||
// multiple properties
|
||||
[
|
||||
{ val1: { $eq: "foo" }, val2: { $eq: "bar" } },
|
||||
'("val1" = ? and "val2" = ?)',
|
||||
["foo", "bar"]
|
||||
],
|
||||
[
|
||||
{ val1: { $eq: "foo" }, val2: { $eq: "bar" } },
|
||||
'("val1" = ? and "val2" = ?)',
|
||||
["foo", "bar"]
|
||||
],
|
||||
|
||||
// or constructs
|
||||
[
|
||||
{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } },
|
||||
'("val1" = ? or "val2" = ?)',
|
||||
["foo", "bar"]
|
||||
],
|
||||
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, '("val1" = ? or "val1" = ?)', [1, 2]],
|
||||
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, '("val1" = ? or "val1" = ?)', [1, 2]]
|
||||
];
|
||||
|
||||
for (const [query, expectedSql, expectedParams] of tests) {
|
||||
const { sql, parameters } = compile(query);
|
||||
expect(sql).toContain(`select * from "t" where ${expectedSql}`);
|
||||
expect(parameters).toEqual(expectedParams);
|
||||
}
|
||||
});
|
||||
|
||||
test("keys", () => {
|
||||
const tests: [WhereQuery, string[]][] = [
|
||||
// multiple constraints per property
|
||||
[{ val: { $lt: 10, $gte: 3 } }, ["val"]],
|
||||
|
||||
// multiple properties
|
||||
[{ val1: { $eq: "foo" }, val2: { $eq: "bar" } }, ["val1", "val2"]],
|
||||
|
||||
// or constructs
|
||||
[{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } }, ["val1", "val2"]],
|
||||
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, ["val1"]]
|
||||
];
|
||||
|
||||
for (const [query, expectedKeys] of tests) {
|
||||
const keys = WhereBuilder.getPropertyNames(query);
|
||||
expect(keys).toEqual(expectedKeys);
|
||||
}
|
||||
});
|
||||
|
||||
test("with", () => {
|
||||
decode({ with: ["posts"] }, { with: { posts: {} } });
|
||||
decode({ with: { posts: {} } }, { with: { posts: {} } });
|
||||
decode({ with: { posts: { limit: 1 } } }, { with: { posts: { limit: 1 } } });
|
||||
decode(
|
||||
{
|
||||
with: {
|
||||
posts: {
|
||||
with: {
|
||||
images: {
|
||||
select: ["id"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
with: {
|
||||
posts: {
|
||||
with: {
|
||||
images: {
|
||||
select: ["id"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// over http
|
||||
{
|
||||
const output = { with: { images: {} } };
|
||||
decode({ with: "images" }, output);
|
||||
decode({ with: '["images"]' }, output);
|
||||
decode({ with: ["images"] }, output);
|
||||
decode({ with: { images: {} } }, output);
|
||||
}
|
||||
|
||||
{
|
||||
const output = { with: { images: {}, comments: {} } };
|
||||
decode({ with: "images,comments" }, output);
|
||||
decode({ with: ["images", "comments"] }, output);
|
||||
decode({ with: '["images", "comments"]' }, output);
|
||||
decode({ with: { images: {}, comments: {} } }, output);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("data-query-impl: Typebox", () => {
|
||||
test("sort", async () => {
|
||||
const _dflt = { sort: { by: "id", dir: "asc" } };
|
||||
|
||||
decode({ sort: "" }, _dflt);
|
||||
decode({ sort: "name" }, { sort: { by: "name", dir: "asc" } });
|
||||
decode({ sort: "-name" }, { sort: { by: "name", dir: "desc" } });
|
||||
decode({ sort: "-posts.name" }, { sort: { by: "posts.name", dir: "desc" } });
|
||||
decode({ sort: "-1name" }, _dflt);
|
||||
decode({ sort: { by: "name", dir: "desc" } }, { sort: { by: "name", dir: "desc" } });
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,6 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
NumberField,
|
||||
PrimaryField,
|
||||
Repository,
|
||||
TextField
|
||||
} from "../../src/data";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { TextField, PrimaryField, NumberField } from "data/fields";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
@@ -18,14 +12,14 @@ describe("some tests", async () => {
|
||||
|
||||
const users = new Entity("users", [
|
||||
new TextField("username", { required: true, default_value: "nobody" }),
|
||||
new TextField("email", { maxLength: 3 })
|
||||
new TextField("email", { maxLength: 3 }),
|
||||
]);
|
||||
|
||||
const posts = new Entity("posts", [
|
||||
new TextField("title"),
|
||||
new TextField("content"),
|
||||
new TextField("created_at"),
|
||||
new NumberField("likes", { default_value: 0 })
|
||||
new NumberField("likes", { default_value: 0 }),
|
||||
]);
|
||||
|
||||
const em = new EntityManager([users, posts], connection);
|
||||
@@ -34,39 +28,32 @@ describe("some tests", async () => {
|
||||
|
||||
test("findId", async () => {
|
||||
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(
|
||||
'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.result).toEqual([]);
|
||||
expect(query.data).toBeUndefined();
|
||||
});
|
||||
|
||||
test("findMany", async () => {
|
||||
const query = await em.repository(users).findMany();
|
||||
|
||||
expect(query.sql).toBe(
|
||||
'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.result).toEqual([]);
|
||||
expect(query.data).toEqual([]);
|
||||
});
|
||||
|
||||
test("findMany with number", async () => {
|
||||
const query = await em.repository(posts).findMany();
|
||||
|
||||
expect(query.sql).toBe(
|
||||
'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.result).toEqual([]);
|
||||
expect(query.data).toEqual([]);
|
||||
});
|
||||
|
||||
test("try adding an existing field name", async () => {
|
||||
@@ -74,7 +61,7 @@ describe("some tests", async () => {
|
||||
new Entity("users", [
|
||||
new TextField("username"),
|
||||
new TextField("email"),
|
||||
new TextField("email") // not throwing, it's just being ignored
|
||||
new TextField("email"), // not throwing, it's just being ignored
|
||||
]);
|
||||
}).toBeDefined();
|
||||
|
||||
@@ -83,7 +70,7 @@ describe("some tests", async () => {
|
||||
new TextField("username"),
|
||||
new TextField("email"),
|
||||
// field config differs, will throw
|
||||
new TextField("email", { required: true })
|
||||
new TextField("email", { required: true }),
|
||||
]);
|
||||
}).toThrow();
|
||||
|
||||
@@ -91,7 +78,7 @@ describe("some tests", async () => {
|
||||
new Entity("users", [
|
||||
new PrimaryField(),
|
||||
new TextField("username"),
|
||||
new TextField("email")
|
||||
new TextField("email"),
|
||||
]);
|
||||
}).toBeDefined();
|
||||
});
|
||||
@@ -110,4 +97,18 @@ describe("some tests", async () => {
|
||||
new EntityManager([entity, entity2], connection);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
test("primary uuid", async () => {
|
||||
const entity = new Entity("users", [
|
||||
new PrimaryField("id", { format: "uuid" }),
|
||||
new TextField("username"),
|
||||
]);
|
||||
const em = new EntityManager([entity], getDummyConnection().dummyConnection);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
const mutator = em.mutator(entity);
|
||||
const data = await mutator.insertOne({ username: "test" });
|
||||
expect(data.data.id).toBeDefined();
|
||||
expect(data.data.id).toBeString();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { unlink } from "node:fs/promises";
|
||||
import type { SqliteDatabase } from "kysely";
|
||||
// @ts-ignore
|
||||
import Database from "libsql";
|
||||
import { SqliteLocalConnection } from "../../src/data";
|
||||
import { SqliteLocalConnection } from "data/connection/sqlite/SqliteLocalConnection";
|
||||
|
||||
export function getDummyDatabase(memory: boolean = true): {
|
||||
dummyDb: SqliteDatabase;
|
||||
@@ -16,7 +16,7 @@ export function getDummyDatabase(memory: boolean = true): {
|
||||
afterAllCleanup: async () => {
|
||||
if (!memory) await unlink(DB_NAME);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export function getDummyConnection(memory: boolean = true) {
|
||||
|
||||
return {
|
||||
dummyConnection,
|
||||
afterAllCleanup
|
||||
afterAllCleanup,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
ManyToOneRelation,
|
||||
NumberField,
|
||||
SchemaManager,
|
||||
TextField
|
||||
} from "../../src/data";
|
||||
import { Entity } from "data/entities";
|
||||
import { EntityManager } from "data/entities/EntityManager";
|
||||
import { ManyToOneRelation } from "data/relations";
|
||||
import { NumberField, TextField } from "data/fields";
|
||||
import { SchemaManager } from "data/schema/SchemaManager";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
@@ -21,7 +18,7 @@ describe("Mutator relation", async () => {
|
||||
const posts = new Entity("posts", [
|
||||
new TextField("title"),
|
||||
new TextField("content", { default_value: "..." }),
|
||||
new NumberField("count", { default_value: 0 })
|
||||
new NumberField("count", { default_value: 0 }),
|
||||
]);
|
||||
|
||||
const users = new Entity("users", [new TextField("username")]);
|
||||
@@ -44,7 +41,7 @@ describe("Mutator relation", async () => {
|
||||
expect(em.mutator(posts).insertOne({ title: "post2", users_id: 10 })).rejects.toThrow();
|
||||
|
||||
expect(
|
||||
em.mutator(posts).insertOne({ title: "post2", users_id: data.id })
|
||||
em.mutator(posts).insertOne({ title: "post2", users_id: data.id }),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { Entity, EntityManager, Mutator, NumberField, TextField } from "../../src/data";
|
||||
import { TransformPersistFailedException } from "../../src/data/errors";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { NumberField, TextField } from "data/fields";
|
||||
import { TransformPersistFailedException } from "data/errors";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
@@ -14,7 +15,7 @@ describe("Mutator simple", async () => {
|
||||
|
||||
const items = new Entity("items", [
|
||||
new TextField("label", { required: true, minLength: 1 }),
|
||||
new NumberField("count", { default_value: 0 })
|
||||
new NumberField("count", { default_value: 0 }),
|
||||
]);
|
||||
const em = new EntityManager<any>([items], connection);
|
||||
|
||||
@@ -29,11 +30,11 @@ describe("Mutator simple", async () => {
|
||||
test("insert single row", async () => {
|
||||
const mutation = await em.mutator(items).insertOne({
|
||||
label: "test",
|
||||
count: 1
|
||||
count: 1,
|
||||
});
|
||||
|
||||
expect(mutation.sql).toBe(
|
||||
'insert into "items" ("count", "label") values (?, ?) returning "id", "label", "count"'
|
||||
'insert into "items" ("count", "label") values (?, ?) returning "id", "label", "count"',
|
||||
);
|
||||
expect(mutation.data).toEqual({ id: 1, label: "test", count: 1 });
|
||||
|
||||
@@ -41,11 +42,11 @@ describe("Mutator simple", async () => {
|
||||
limit: 1,
|
||||
sort: {
|
||||
by: "id",
|
||||
dir: "desc"
|
||||
}
|
||||
dir: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
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 () => {
|
||||
@@ -53,18 +54,18 @@ describe("Mutator simple", async () => {
|
||||
limit: 1,
|
||||
sort: {
|
||||
by: "id",
|
||||
dir: "desc"
|
||||
}
|
||||
dir: "desc",
|
||||
},
|
||||
});
|
||||
const id = query.data![0].id as number;
|
||||
|
||||
const mutation = await em.mutator(items).updateOne(id, {
|
||||
label: "new label",
|
||||
count: 100
|
||||
count: 100,
|
||||
});
|
||||
|
||||
expect(mutation.sql).toBe(
|
||||
'update "items" set "label" = ?, "count" = ? where "id" = ? returning "id", "label", "count"'
|
||||
'update "items" set "label" = ?, "count" = ? where "id" = ? returning "id", "label", "count"',
|
||||
);
|
||||
expect(mutation.data).toEqual({ id, label: "new label", count: 100 });
|
||||
});
|
||||
@@ -74,27 +75,27 @@ describe("Mutator simple", async () => {
|
||||
limit: 1,
|
||||
sort: {
|
||||
by: "id",
|
||||
dir: "desc"
|
||||
}
|
||||
dir: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
const id = query.data![0].id as number;
|
||||
const mutation = await em.mutator(items).deleteOne(id);
|
||||
|
||||
expect(mutation.sql).toBe(
|
||||
'delete from "items" where "id" = ? returning "id", "label", "count"'
|
||||
'delete from "items" where "id" = ? returning "id", "label", "count"',
|
||||
);
|
||||
expect(mutation.data).toEqual({ id, label: "new label", count: 100 });
|
||||
|
||||
const query2 = await em.repository(items).findId(id);
|
||||
expect(query2.result.length).toBe(0);
|
||||
expect(query2.data).toBeUndefined();
|
||||
});
|
||||
|
||||
test("validation: insert incomplete row", async () => {
|
||||
const incompleteCreate = async () =>
|
||||
await em.mutator(items).insertOne({
|
||||
//label: "test",
|
||||
count: 1
|
||||
count: 1,
|
||||
});
|
||||
|
||||
expect(incompleteCreate()).rejects.toThrow();
|
||||
@@ -104,7 +105,7 @@ describe("Mutator simple", async () => {
|
||||
const invalidCreate1 = async () =>
|
||||
await em.mutator(items).insertOne({
|
||||
label: 111, // this should work
|
||||
count: "1" // this should fail
|
||||
count: "1", // this should fail
|
||||
});
|
||||
|
||||
expect(invalidCreate1()).rejects.toThrow(TransformPersistFailedException);
|
||||
@@ -112,7 +113,7 @@ describe("Mutator simple", async () => {
|
||||
const invalidCreate2 = async () =>
|
||||
await em.mutator(items).insertOne({
|
||||
label: "", // this should fail
|
||||
count: 1
|
||||
count: 1,
|
||||
});
|
||||
|
||||
expect(invalidCreate2()).rejects.toThrow(TransformPersistFailedException);
|
||||
@@ -137,7 +138,7 @@ describe("Mutator simple", async () => {
|
||||
expect((await em.repository(items).findMany()).data.length).toBe(data.length - 2);
|
||||
//console.log((await em.repository(items).findMany()).data);
|
||||
|
||||
await em.mutator(items).deleteWhere();
|
||||
await em.mutator(items).deleteWhere({ id: { $isnull: 0 } });
|
||||
expect((await em.repository(items).findMany()).data.length).toBe(0);
|
||||
|
||||
//expect(res.data.count).toBe(0);
|
||||
@@ -152,38 +153,38 @@ describe("Mutator simple", async () => {
|
||||
await em.mutator(items).updateWhere(
|
||||
{ count: 2 },
|
||||
{
|
||||
count: 10
|
||||
}
|
||||
count: 10,
|
||||
},
|
||||
);
|
||||
expect((await em.repository(items).findMany()).data).toEqual([
|
||||
{ id: 6, label: "update", count: 1 },
|
||||
{ id: 7, label: "update too", count: 1 },
|
||||
{ id: 8, label: "keep", count: 0 }
|
||||
{ id: 8, label: "keep", count: 0 },
|
||||
]);
|
||||
|
||||
// expect 2 to be updated
|
||||
await em.mutator(items).updateWhere(
|
||||
{ count: 2 },
|
||||
{
|
||||
count: 1
|
||||
}
|
||||
count: 1,
|
||||
},
|
||||
);
|
||||
|
||||
expect((await em.repository(items).findMany()).data).toEqual([
|
||||
{ id: 6, label: "update", count: 2 },
|
||||
{ id: 7, label: "update too", count: 2 },
|
||||
{ id: 8, label: "keep", count: 0 }
|
||||
{ id: 8, label: "keep", count: 0 },
|
||||
]);
|
||||
});
|
||||
|
||||
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 { data } = await em.mutator(items).insertMany(inserts);
|
||||
|
||||
expect(data.length).toBe(2);
|
||||
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);
|
||||
|
||||
const { data: data2 } = await em.repo(items).findMany({ offset: oldCount });
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { afterAll, expect as bunExpect, describe, test } from "bun:test";
|
||||
import { stripMark } from "../../src/core/utils";
|
||||
import { Entity, EntityManager, PolymorphicRelation, TextField } from "../../src/data";
|
||||
import { stripMark } from "core/utils/schema";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { TextField } from "data/fields";
|
||||
import { PolymorphicRelation } from "data/relations";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
@@ -23,23 +25,23 @@ describe("Polymorphic", async () => {
|
||||
source: "categories",
|
||||
target: "media",
|
||||
config: {
|
||||
mappedBy: "image"
|
||||
}
|
||||
mappedBy: "image",
|
||||
},
|
||||
});
|
||||
// media should not see categories
|
||||
expect(em.relationsOf(media.name).map((r) => r.toJSON())).toEqual([]);
|
||||
|
||||
// it's important that media cannot access categories
|
||||
expect(em.relations.targetRelationsOf(categories).map((r) => r.source.entity.name)).toEqual(
|
||||
[]
|
||||
[],
|
||||
);
|
||||
expect(em.relations.targetRelationsOf(media).map((r) => r.source.entity.name)).toEqual([]);
|
||||
|
||||
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.entity.name)).toEqual([
|
||||
"media"
|
||||
"media",
|
||||
]);
|
||||
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.reference)).toEqual([
|
||||
"image"
|
||||
"image",
|
||||
]);
|
||||
expect(em.relations.sourceRelationsOf(media).map((r) => r.target.entity.name)).toEqual([]);
|
||||
|
||||
@@ -48,7 +50,7 @@ describe("Polymorphic", async () => {
|
||||
"id",
|
||||
"path",
|
||||
"reference",
|
||||
"entity_id"
|
||||
"entity_id",
|
||||
]);
|
||||
expect(media.getSelect()).toEqual(["id", "path"]);
|
||||
});
|
||||
@@ -60,7 +62,7 @@ describe("Polymorphic", async () => {
|
||||
const entities = [media, categories];
|
||||
const single = new PolymorphicRelation(categories, media, {
|
||||
mappedBy: "single",
|
||||
targetCardinality: 1
|
||||
targetCardinality: 1,
|
||||
});
|
||||
const multiple = new PolymorphicRelation(categories, media, { mappedBy: "multiple" });
|
||||
|
||||
@@ -71,17 +73,17 @@ describe("Polymorphic", async () => {
|
||||
|
||||
// it's important that media cannot access categories
|
||||
expect(em.relations.targetRelationsOf(categories).map((r) => r.source.entity.name)).toEqual(
|
||||
[]
|
||||
[],
|
||||
);
|
||||
expect(em.relations.targetRelationsOf(media).map((r) => r.source.entity.name)).toEqual([]);
|
||||
|
||||
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.entity.name)).toEqual([
|
||||
"media",
|
||||
"media"
|
||||
"media",
|
||||
]);
|
||||
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.reference)).toEqual([
|
||||
"single",
|
||||
"multiple"
|
||||
"multiple",
|
||||
]);
|
||||
expect(em.relations.sourceRelationsOf(media).map((r) => r.target.entity.name)).toEqual([]);
|
||||
|
||||
@@ -90,7 +92,7 @@ describe("Polymorphic", async () => {
|
||||
"id",
|
||||
"path",
|
||||
"reference",
|
||||
"entity_id"
|
||||
"entity_id",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,19 +2,20 @@ import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
BooleanField,
|
||||
DateField,
|
||||
Entity,
|
||||
EntityIndex,
|
||||
EntityManager,
|
||||
EnumField,
|
||||
JsonField,
|
||||
NumberField,
|
||||
TextField,
|
||||
EntityIndex,
|
||||
} from "data/fields";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import {
|
||||
ManyToManyRelation,
|
||||
ManyToOneRelation,
|
||||
NumberField,
|
||||
OneToOneRelation,
|
||||
PolymorphicRelation,
|
||||
TextField
|
||||
} from "../../src/data";
|
||||
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
||||
} from "data/relations";
|
||||
import { DummyConnection } from "data/connection/DummyConnection";
|
||||
import {
|
||||
FieldPrototype,
|
||||
type FieldSchema,
|
||||
@@ -31,9 +32,9 @@ import {
|
||||
medium,
|
||||
number,
|
||||
relation,
|
||||
text
|
||||
} from "../../src/data/prototype";
|
||||
import { MediaField } from "../../src/media/MediaField";
|
||||
text,
|
||||
} from "data/prototype";
|
||||
import { MediaField } from "media/MediaField";
|
||||
|
||||
describe("prototype", () => {
|
||||
test("...", () => {
|
||||
@@ -54,7 +55,7 @@ describe("prototype", () => {
|
||||
name: text(),
|
||||
bio: text(),
|
||||
age: number(),
|
||||
some: number()
|
||||
some: number(),
|
||||
});
|
||||
type db = {
|
||||
users: Schema<typeof users>;
|
||||
@@ -70,7 +71,7 @@ describe("prototype", () => {
|
||||
name: text({ default_value: "hello" }).required(),
|
||||
bio: text(),
|
||||
age: number(),
|
||||
some: number().required()
|
||||
some: number().required(),
|
||||
});
|
||||
|
||||
const obj: InsertSchema<typeof user> = { name: "yo", some: 1 };
|
||||
@@ -83,12 +84,12 @@ describe("prototype", () => {
|
||||
new TextField("title", { required: true }),
|
||||
new TextField("content"),
|
||||
new DateField("created_at", {
|
||||
type: "datetime"
|
||||
type: "datetime",
|
||||
}),
|
||||
// @ts-ignore
|
||||
new MediaField("images", { entity: "posts" }),
|
||||
// @ts-ignore
|
||||
new MediaField("cover", { entity: "posts", max_items: 1 })
|
||||
new MediaField("cover", { entity: "posts", max_items: 1 }),
|
||||
]);
|
||||
|
||||
const posts2 = entity("posts", {
|
||||
@@ -96,12 +97,13 @@ describe("prototype", () => {
|
||||
content: text(),
|
||||
created_at: datetime(),
|
||||
images: media(),
|
||||
cover: medium()
|
||||
cover: medium(),
|
||||
});
|
||||
|
||||
type Posts = Schema<typeof posts2>;
|
||||
|
||||
expect(posts1.toJSON()).toEqual(posts2.toJSON());
|
||||
// @todo: check
|
||||
//expect(posts1.toJSON()).toEqual(posts2.toJSON());
|
||||
});
|
||||
|
||||
test("test example", async () => {
|
||||
@@ -117,11 +119,11 @@ describe("prototype", () => {
|
||||
type: "objects",
|
||||
values: [
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "inactive", label: "Not active" }
|
||||
]
|
||||
}
|
||||
{ value: "inactive", label: "Not active" },
|
||||
],
|
||||
},
|
||||
}),
|
||||
new JsonField("json")
|
||||
new JsonField("json"),
|
||||
]);
|
||||
|
||||
const test2 = entity("test", {
|
||||
@@ -134,10 +136,10 @@ describe("prototype", () => {
|
||||
status: enumm<"active" | "inactive">({
|
||||
enum: [
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "inactive", label: "Not active" }
|
||||
]
|
||||
{ value: "inactive", label: "Not active" },
|
||||
],
|
||||
}),
|
||||
json: json<{ some: number }>()
|
||||
json: json<{ some: number }>(),
|
||||
});
|
||||
|
||||
expect(test.toJSON()).toEqual(test2.toJSON());
|
||||
@@ -161,12 +163,12 @@ describe("prototype", () => {
|
||||
// category has single image
|
||||
new PolymorphicRelation(categories, _media, {
|
||||
mappedBy: "image",
|
||||
targetCardinality: 1
|
||||
targetCardinality: 1,
|
||||
}),
|
||||
|
||||
// post has multiple images
|
||||
new PolymorphicRelation(posts, _media, { mappedBy: "images" }),
|
||||
new PolymorphicRelation(posts, _media, { mappedBy: "cover", targetCardinality: 1 })
|
||||
new PolymorphicRelation(posts, _media, { mappedBy: "cover", targetCardinality: 1 }),
|
||||
];
|
||||
|
||||
const relations2 = [
|
||||
@@ -180,7 +182,7 @@ describe("prototype", () => {
|
||||
relation(categories).polyToOne(_media, { mappedBy: "image" }),
|
||||
|
||||
relation(posts).polyToMany(_media, { mappedBy: "images" }),
|
||||
relation(posts).polyToOne(_media, { mappedBy: "cover" })
|
||||
relation(posts).polyToOne(_media, { mappedBy: "cover" }),
|
||||
];
|
||||
|
||||
expect(relations.map((r) => r.toJSON())).toEqual(relations2.map((r) => r.toJSON()));
|
||||
@@ -194,21 +196,21 @@ describe("prototype", () => {
|
||||
posts,
|
||||
categories,
|
||||
{
|
||||
connectionTableMappedName: "custom"
|
||||
connectionTableMappedName: "custom",
|
||||
},
|
||||
[new TextField("description")]
|
||||
[new TextField("description")],
|
||||
);
|
||||
|
||||
const fields = {
|
||||
description: text()
|
||||
description: text(),
|
||||
};
|
||||
let o: FieldSchema<typeof fields>;
|
||||
const rel2 = relation(posts).manyToMany(
|
||||
categories,
|
||||
{
|
||||
connectionTableMappedName: "custom"
|
||||
connectionTableMappedName: "custom",
|
||||
},
|
||||
fields
|
||||
fields,
|
||||
);
|
||||
|
||||
expect(rel.toJSON()).toEqual(rel2.toJSON());
|
||||
@@ -216,11 +218,11 @@ describe("prototype", () => {
|
||||
|
||||
test("devexample", async () => {
|
||||
const users = entity("users", {
|
||||
username: text()
|
||||
username: text(),
|
||||
});
|
||||
|
||||
const comments = entity("comments", {
|
||||
content: text()
|
||||
content: text(),
|
||||
});
|
||||
|
||||
const posts = entity("posts", {
|
||||
@@ -228,17 +230,17 @@ describe("prototype", () => {
|
||||
content: text(),
|
||||
created_at: datetime(),
|
||||
images: media(),
|
||||
cover: medium()
|
||||
cover: medium(),
|
||||
});
|
||||
|
||||
const categories = entity("categories", {
|
||||
name: text(),
|
||||
description: text(),
|
||||
image: medium()
|
||||
image: medium(),
|
||||
});
|
||||
|
||||
const settings = entity("settings", {
|
||||
theme: text()
|
||||
theme: text(),
|
||||
});
|
||||
|
||||
const test = entity("test", {
|
||||
@@ -251,10 +253,10 @@ describe("prototype", () => {
|
||||
status: enumm<"active" | "inactive">({
|
||||
enum: [
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "inactive", label: "Not active" }
|
||||
]
|
||||
{ value: "inactive", label: "Not active" },
|
||||
],
|
||||
}),
|
||||
json: json<{ some: number }>()
|
||||
json: json<{ some: number }>(),
|
||||
});
|
||||
|
||||
const _media = entity("media", {});
|
||||
@@ -270,7 +272,7 @@ describe("prototype", () => {
|
||||
relation(users).oneToOne(settings),
|
||||
|
||||
relation(comments).manyToOne(users, { required: true }),
|
||||
relation(comments).manyToOne(posts, { required: true })
|
||||
relation(comments).manyToOne(posts, { required: true }),
|
||||
];
|
||||
|
||||
const obj: Schema<typeof test> = {} as any;
|
||||
@@ -281,12 +283,12 @@ describe("prototype", () => {
|
||||
{
|
||||
posts: entity("posts", { name: text(), slug: text().required() }),
|
||||
comments: entity("comments", { some: text() }),
|
||||
users: entity("users", { email: text() })
|
||||
users: entity("users", { email: text() }),
|
||||
},
|
||||
({ relation, index }, { posts, comments, users }) => {
|
||||
relation(posts).manyToOne(comments).manyToOne(users);
|
||||
index(posts).on(["name"]).on(["slug"], true);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
type LocalDb = (typeof _em)["DB"];
|
||||
@@ -294,16 +296,16 @@ describe("prototype", () => {
|
||||
const es = [
|
||||
new Entity("posts", [new TextField("name"), new TextField("slug", { required: true })]),
|
||||
new Entity("comments", [new TextField("some")]),
|
||||
new Entity("users", [new TextField("email")])
|
||||
];
|
||||
new Entity("users", [new TextField("email")]),
|
||||
] as const;
|
||||
const _em2 = new EntityManager(
|
||||
es,
|
||||
[...es],
|
||||
new DummyConnection(),
|
||||
[new ManyToOneRelation(es[0], es[1]), new ManyToOneRelation(es[0], es[2])],
|
||||
[
|
||||
new EntityIndex(es[0], [es[0].field("name")!]),
|
||||
new EntityIndex(es[0], [es[0].field("slug")!], true)
|
||||
]
|
||||
new EntityIndex(es[0], [es[0].field("slug")!], true),
|
||||
],
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { Entity, EntityManager, TextField } from "../../src/data";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { TextField } from "data/fields";
|
||||
import {
|
||||
ManyToManyRelation,
|
||||
ManyToOneRelation,
|
||||
OneToOneRelation,
|
||||
PolymorphicRelation,
|
||||
RelationField
|
||||
} from "../../src/data/relations";
|
||||
RelationField,
|
||||
} from "data/relations";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
@@ -22,32 +23,32 @@ describe("Relations", async () => {
|
||||
const r1 = new RelationField("users_id", {
|
||||
reference: "users",
|
||||
target: "users",
|
||||
target_field: "id"
|
||||
target_field: "id",
|
||||
});
|
||||
|
||||
const sql1 = schema
|
||||
.createTable("posts")
|
||||
.addColumn(...r1.schema()!)
|
||||
.addColumn(...em.connection.getFieldSchema(r1.schema())!)
|
||||
.compile().sql;
|
||||
|
||||
expect(sql1).toBe(
|
||||
'create table "posts" ("users_id" integer references "users" ("id") on delete set null)'
|
||||
'create table "posts" ("users_id" integer references "users" ("id") on delete set null)',
|
||||
);
|
||||
|
||||
//const r2 = new RelationField(new Entity("users"), "author");
|
||||
const r2 = new RelationField("author_id", {
|
||||
reference: "author",
|
||||
target: "users",
|
||||
target_field: "id"
|
||||
target_field: "id",
|
||||
});
|
||||
|
||||
const sql2 = schema
|
||||
.createTable("posts")
|
||||
.addColumn(...r2.schema()!)
|
||||
.addColumn(...em.connection.getFieldSchema(r2.schema())!)
|
||||
.compile().sql;
|
||||
|
||||
expect(sql2).toBe(
|
||||
'create table "posts" ("author_id" integer references "users" ("id") on delete set null)'
|
||||
'create table "posts" ("author_id" integer references "users" ("id") on delete set null)',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -57,7 +58,7 @@ describe("Relations", async () => {
|
||||
reference: "users",
|
||||
target: "users",
|
||||
target_field: "id",
|
||||
required: true
|
||||
required: true,
|
||||
});
|
||||
expect(r1.isRequired()).toBeTrue();
|
||||
});
|
||||
@@ -66,8 +67,8 @@ describe("Relations", async () => {
|
||||
const users = new Entity("users", [new TextField("username")]);
|
||||
const posts = new Entity("posts", [
|
||||
new TextField("title", {
|
||||
maxLength: 2
|
||||
})
|
||||
maxLength: 2,
|
||||
}),
|
||||
]);
|
||||
|
||||
const entities = [users, posts];
|
||||
@@ -77,7 +78,7 @@ describe("Relations", async () => {
|
||||
const em = new EntityManager(entities, dummyConnection, relations);
|
||||
|
||||
// verify naming
|
||||
const rel = em.relations.all[0];
|
||||
const rel = em.relations.all[0]!;
|
||||
expect(rel.source.entity.name).toBe(posts.name);
|
||||
expect(rel.source.reference).toBe(posts.name);
|
||||
expect(rel.target.entity.name).toBe(users.name);
|
||||
@@ -89,11 +90,11 @@ describe("Relations", async () => {
|
||||
// verify low level relation
|
||||
expect(em.relationsOf(users.name).length).toBe(1);
|
||||
expect(em.relationsOf(users.name).length).toBe(1);
|
||||
expect(em.relationsOf(users.name)[0].source.entity).toBe(posts);
|
||||
expect(em.relationsOf(users.name)[0]!.source.entity).toBe(posts);
|
||||
expect(posts.field("author_id")).toBeInstanceOf(RelationField);
|
||||
expect(em.relationsOf(users.name).length).toBe(1);
|
||||
expect(em.relationsOf(users.name).length).toBe(1);
|
||||
expect(em.relationsOf(users.name)[0].source.entity).toBe(posts);
|
||||
expect(em.relationsOf(users.name)[0]!.source.entity).toBe(posts);
|
||||
|
||||
// verify high level relation (from users)
|
||||
const userPostsRel = em.relationOf(users.name, "posts");
|
||||
@@ -122,7 +123,7 @@ describe("Relations", async () => {
|
||||
.selectFrom(users.name)
|
||||
.select((eb) => postAuthorRel.buildWith(users, "posts")(eb).as("posts"));
|
||||
expect(selectPostsFromUsers.compile().sql).toBe(
|
||||
'select (select from "posts" as "posts" where "posts"."author_id" = "users"."id") as "posts" from "users"'
|
||||
'select (select from "posts" as "posts" where "posts"."author_id" = "users"."id") as "posts" from "users"',
|
||||
);
|
||||
expect(postAuthorRel!.getField()).toBeInstanceOf(RelationField);
|
||||
const userObj = { id: 1, username: "test" };
|
||||
@@ -142,7 +143,7 @@ describe("Relations", async () => {
|
||||
.select((eb) => postAuthorRel.buildWith(posts, "author")(eb).as("author"));
|
||||
|
||||
expect(selectUsersFromPosts.compile().sql).toBe(
|
||||
'select (select from "users" as "author" where "author"."id" = "posts"."author_id" limit ?) as "author" from "posts"'
|
||||
'select (select from "users" as "author" where "author"."id" = "posts"."author_id" limit ?) as "author" from "posts"',
|
||||
);
|
||||
expect(postAuthorRel.getField()).toBeInstanceOf(RelationField);
|
||||
const postObj = { id: 1, title: "test" };
|
||||
@@ -158,7 +159,7 @@ describe("Relations", async () => {
|
||||
$detach: false,
|
||||
primary: undefined,
|
||||
cardinality: undefined,
|
||||
relation_type: "n:1"
|
||||
relation_type: "n:1",
|
||||
});
|
||||
|
||||
expect(postAuthorRel!.helper(posts.name)!.getMutationInfo()).toEqual({
|
||||
@@ -170,7 +171,7 @@ describe("Relations", async () => {
|
||||
$detach: false,
|
||||
primary: "id",
|
||||
cardinality: 1,
|
||||
relation_type: "n:1"
|
||||
relation_type: "n:1",
|
||||
});
|
||||
|
||||
/*console.log("ManyToOne (source=posts, target=users)");
|
||||
@@ -191,7 +192,7 @@ describe("Relations", async () => {
|
||||
const em = new EntityManager(entities, dummyConnection, relations);
|
||||
|
||||
// verify naming
|
||||
const rel = em.relations.all[0];
|
||||
const rel = em.relations.all[0]!;
|
||||
expect(rel.source.entity.name).toBe(users.name);
|
||||
expect(rel.source.reference).toBe(users.name);
|
||||
expect(rel.target.entity.name).toBe(settings.name);
|
||||
@@ -202,8 +203,8 @@ describe("Relations", async () => {
|
||||
|
||||
expect(em.relationsOf(users.name).length).toBe(1);
|
||||
expect(em.relationsOf(users.name).length).toBe(1);
|
||||
expect(em.relationsOf(users.name)[0].source.entity).toBe(users);
|
||||
expect(em.relationsOf(users.name)[0].target.entity).toBe(settings);
|
||||
expect(em.relationsOf(users.name)[0]!.source.entity).toBe(users);
|
||||
expect(em.relationsOf(users.name)[0]!.target.entity).toBe(settings);
|
||||
|
||||
// verify high level relation (from users)
|
||||
const userSettingRel = em.relationOf(users.name, settings.name);
|
||||
@@ -225,7 +226,7 @@ describe("Relations", async () => {
|
||||
$detach: false,
|
||||
primary: "id",
|
||||
cardinality: 1,
|
||||
relation_type: "1:1"
|
||||
relation_type: "1:1",
|
||||
});
|
||||
expect(userSettingRel!.helper(settings.name)!.getMutationInfo()).toEqual({
|
||||
reference: "users",
|
||||
@@ -236,7 +237,7 @@ describe("Relations", async () => {
|
||||
$detach: false,
|
||||
primary: undefined,
|
||||
cardinality: 1,
|
||||
relation_type: "1:1"
|
||||
relation_type: "1:1",
|
||||
});
|
||||
|
||||
/*console.log("");
|
||||
@@ -310,20 +311,20 @@ describe("Relations", async () => {
|
||||
*/
|
||||
const selectCategoriesFromPosts = kysely
|
||||
.selectFrom(posts.name)
|
||||
.select((eb) => postCategoriesRel.buildWith(posts)(eb).as("categories"));
|
||||
.select((eb) => postCategoriesRel.buildWith(posts)(eb).select("id").as("categories"));
|
||||
expect(selectCategoriesFromPosts.compile().sql).toBe(
|
||||
'select (select "categories"."id" as "id", "categories"."label" as "label" from "categories" inner join "posts_categories" on "categories"."id" = "posts_categories"."categories_id" where "posts"."id" = "posts_categories"."posts_id" limit ?) as "categories" from "posts"'
|
||||
'select (select "id" from "categories" inner join "posts_categories" on "categories"."id" = "posts_categories"."categories_id" where "posts"."id" = "posts_categories"."posts_id" limit ?) as "categories" from "posts"',
|
||||
);
|
||||
|
||||
const selectPostsFromCategories = kysely
|
||||
.selectFrom(categories.name)
|
||||
.select((eb) => postCategoriesRel.buildWith(categories)(eb).as("posts"));
|
||||
.select((eb) => postCategoriesRel.buildWith(categories)(eb).select("id").as("posts"));
|
||||
expect(selectPostsFromCategories.compile().sql).toBe(
|
||||
'select (select "posts"."id" as "id", "posts"."title" as "title" from "posts" inner join "posts_categories" on "posts"."id" = "posts_categories"."posts_id" where "categories"."id" = "posts_categories"."categories_id" limit ?) as "posts" from "categories"'
|
||||
'select (select "id" from "posts" inner join "posts_categories" on "posts"."id" = "posts_categories"."posts_id" where "categories"."id" = "posts_categories"."categories_id" limit ?) as "posts" from "categories"',
|
||||
);
|
||||
|
||||
// mutation info
|
||||
expect(relations[0].helper(posts.name)!.getMutationInfo()).toEqual({
|
||||
expect(relations[0]!.helper(posts.name)!.getMutationInfo()).toEqual({
|
||||
reference: "categories",
|
||||
local_field: undefined,
|
||||
$set: false,
|
||||
@@ -332,9 +333,9 @@ describe("Relations", async () => {
|
||||
$detach: true,
|
||||
primary: "id",
|
||||
cardinality: undefined,
|
||||
relation_type: "m:n"
|
||||
relation_type: "m:n",
|
||||
});
|
||||
expect(relations[0].helper(categories.name)!.getMutationInfo()).toEqual({
|
||||
expect(relations[0]!.helper(categories.name)!.getMutationInfo()).toEqual({
|
||||
reference: "posts",
|
||||
local_field: undefined,
|
||||
$set: false,
|
||||
@@ -343,7 +344,7 @@ describe("Relations", async () => {
|
||||
$detach: false,
|
||||
primary: undefined,
|
||||
cardinality: undefined,
|
||||
relation_type: "m:n"
|
||||
relation_type: "m:n",
|
||||
});
|
||||
|
||||
/*console.log("");
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Entity, NumberField, TextField } from "../../../src/data";
|
||||
import { Entity } from "data/entities";
|
||||
import { NumberField, TextField } from "data/fields";
|
||||
|
||||
describe("[data] Entity", async () => {
|
||||
const entity = new Entity("test", [
|
||||
new TextField("name", { required: true }),
|
||||
new TextField("description"),
|
||||
new NumberField("age", { fillable: false, default_value: 18 }),
|
||||
new TextField("hidden", { hidden: true, default_value: "secret" })
|
||||
new TextField("hidden", { hidden: true, default_value: "secret" }),
|
||||
]);
|
||||
|
||||
test("getSelect", async () => {
|
||||
@@ -17,7 +18,7 @@ describe("[data] Entity", async () => {
|
||||
expect(entity.getFillableFields().map((f) => f.name)).toEqual([
|
||||
"name",
|
||||
"description",
|
||||
"hidden"
|
||||
"hidden",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -28,7 +29,7 @@ describe("[data] Entity", async () => {
|
||||
test("getDefaultObject", async () => {
|
||||
expect(entity.getDefaultObject()).toEqual({
|
||||
age: 18,
|
||||
hidden: "secret"
|
||||
hidden: "secret",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,14 +48,7 @@ describe("[data] Entity", async () => {
|
||||
expect(entity.getField("new_field")).toBe(field);
|
||||
});
|
||||
|
||||
// @todo: move this to ClientApp
|
||||
/*test("serialize and deserialize", async () => {
|
||||
const json = entity.toJSON();
|
||||
//sconsole.log("json", json.fields);
|
||||
const newEntity = Entity.deserialize(json);
|
||||
//console.log("newEntity", newEntity.toJSON().fields);
|
||||
expect(newEntity).toBeInstanceOf(Entity);
|
||||
expect(json).toEqual(newEntity.toJSON());
|
||||
expect(json.fields).toEqual(newEntity.toJSON().fields);
|
||||
});*/
|
||||
test.only("types", async () => {
|
||||
console.log(entity.toTypes());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
ManyToManyRelation,
|
||||
ManyToOneRelation,
|
||||
SchemaManager
|
||||
} from "../../../src/data";
|
||||
import { UnableToConnectException } from "../../../src/data/errors";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { ManyToManyRelation, ManyToOneRelation } from "data/relations";
|
||||
import { SchemaManager } from "data/schema/SchemaManager";
|
||||
import { UnableToConnectException } from "data/errors";
|
||||
import { getDummyConnection } from "../helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
@@ -25,7 +21,7 @@ describe("[data] EntityManager", async () => {
|
||||
expect(await em.ping()).toBe(true);
|
||||
expect(() => em.entity("...")).toThrow();
|
||||
expect(() =>
|
||||
em.addRelation(new ManyToOneRelation(new Entity("1"), new Entity("2")))
|
||||
em.addRelation(new ManyToOneRelation(new Entity("1"), new Entity("2"))),
|
||||
).toThrow();
|
||||
expect(em.schema()).toBeInstanceOf(SchemaManager);
|
||||
|
||||
@@ -47,8 +43,8 @@ describe("[data] EntityManager", async () => {
|
||||
em.addRelation(new ManyToOneRelation(posts, users));
|
||||
expect(em.relations.all.length).toBe(1);
|
||||
expect(em.relations.all[0]).toBeInstanceOf(ManyToOneRelation);
|
||||
expect(em.relationsOf("users")).toEqual([em.relations.all[0]]);
|
||||
expect(em.relationsOf("posts")).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.hasRelations("users")).toBe(true);
|
||||
expect(em.hasRelations("posts")).toBe(true);
|
||||
expect(em.relatedEntitiesOf("users")).toEqual([posts]);
|
||||
@@ -98,7 +94,7 @@ describe("[data] EntityManager", async () => {
|
||||
expect(userTargetRel.map((r) => r.other(users).entity.name)).toEqual(["posts", "comments"]);
|
||||
expect(postTargetRel.map((r) => r.other(posts).entity.name)).toEqual([
|
||||
"comments",
|
||||
"categories"
|
||||
"categories",
|
||||
]);
|
||||
expect(commentTargetRel.map((r) => r.other(comments).entity.name)).toEqual([]);
|
||||
expect(categoriesTargetRel.map((r) => r.other(categories).entity.name)).toEqual(["posts"]);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { Entity, EntityManager, ManyToOneRelation, TextField } from "../../../src/data";
|
||||
import { JoinBuilder } from "../../../src/data/entities/query/JoinBuilder";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { ManyToOneRelation } from "data/relations";
|
||||
import { TextField } from "data/fields";
|
||||
import { JoinBuilder } from "data/entities/query/JoinBuilder";
|
||||
import { getDummyConnection } from "../helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
@@ -12,7 +14,7 @@ describe("[data] JoinBuilder", async () => {
|
||||
const em = new EntityManager([users], dummyConnection);
|
||||
|
||||
expect(() =>
|
||||
JoinBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, ["posts"])
|
||||
JoinBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, ["posts"]),
|
||||
).toThrow('Relation "posts" not found');
|
||||
});
|
||||
|
||||
@@ -23,7 +25,7 @@ describe("[data] JoinBuilder", async () => {
|
||||
const em = new EntityManager([users, posts], dummyConnection, relations);
|
||||
|
||||
const qb = JoinBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, [
|
||||
"posts"
|
||||
"posts",
|
||||
]);
|
||||
|
||||
const res = qb.compile();
|
||||
@@ -34,7 +36,7 @@ describe("[data] JoinBuilder", async () => {
|
||||
);*/
|
||||
|
||||
const qb2 = JoinBuilder.addClause(em, em.connection.kysely.selectFrom("posts"), posts, [
|
||||
"author"
|
||||
"author",
|
||||
]);
|
||||
|
||||
const res2 = qb2.compile();
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
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 { Entity, EntityManager } from "data/entities";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
ManyToOneRelation,
|
||||
MutatorEvents,
|
||||
NumberField,
|
||||
OneToOneRelation,
|
||||
type RelationField,
|
||||
RelationField,
|
||||
RelationMutator,
|
||||
TextField
|
||||
} from "../../../src/data";
|
||||
import * as proto from "../../../src/data/prototype";
|
||||
import { getDummyConnection } from "../helper";
|
||||
} from "data/relations";
|
||||
import { NumberField, TextField } from "data/fields";
|
||||
import * as proto from "data/prototype";
|
||||
import { getDummyConnection, disableConsoleLog, enableConsoleLog } from "../../helper";
|
||||
import { MutatorEvents } from "data/events";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
afterAll(afterAllCleanup);
|
||||
|
||||
beforeAll(() => disableConsoleLog(["log", "warn"]));
|
||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||
|
||||
describe("[data] Mutator (base)", async () => {
|
||||
const entity = new Entity("items", [
|
||||
new TextField("label", { required: true }),
|
||||
new NumberField("count"),
|
||||
new TextField("hidden", { hidden: true }),
|
||||
new TextField("not_fillable", { fillable: false })
|
||||
new TextField("not_fillable", { fillable: false }),
|
||||
]);
|
||||
const em = new EntityManager<any>([entity], dummyConnection);
|
||||
await em.schema().sync({ force: true });
|
||||
@@ -44,7 +45,7 @@ describe("[data] Mutator (base)", async () => {
|
||||
test("updateOne", async () => {
|
||||
const { data } = await em.mutator(entity).insertOne(payload);
|
||||
const updated = await em.mutator(entity).updateOne(data.id, {
|
||||
count: 2
|
||||
count: 2,
|
||||
});
|
||||
|
||||
expect(updated.parameters).toEqual([2, data.id]);
|
||||
@@ -77,7 +78,7 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
||||
|
||||
// persisting relational field should just return key value to be added
|
||||
expect(
|
||||
postRelMutator.persistRelationField(postRelField, "users_id", userData.data.id)
|
||||
postRelMutator.persistRelationField(postRelField, "users_id", userData.data.id),
|
||||
).resolves.toEqual(["users_id", userData.data.id]);
|
||||
|
||||
// persisting invalid value should throw
|
||||
@@ -86,8 +87,8 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
||||
// persisting reference should ...
|
||||
expect(
|
||||
postRelMutator.persistReference(relations[0]!, "users", {
|
||||
$set: { id: userData.data.id }
|
||||
})
|
||||
$set: { id: userData.data.id },
|
||||
}),
|
||||
).resolves.toEqual(["users_id", userData.data.id]);
|
||||
// @todo: add what methods are allowed to relation, like $create should not be allowed for post<>users
|
||||
|
||||
@@ -99,8 +100,8 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
||||
expect(
|
||||
em.mutator(posts).insertOne({
|
||||
title: "post1",
|
||||
users_id: 100 // user does not exist yet
|
||||
})
|
||||
users_id: 100, // user does not exist yet
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
@@ -111,7 +112,7 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
||||
const em = new EntityManager([items, cats], dummyConnection, relations);
|
||||
|
||||
expect(em.mutator(items).insertOne({ label: "test" })).rejects.toThrow(
|
||||
'Field "cats_id" is required'
|
||||
'Field "cats_id" is required',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -119,14 +120,14 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
||||
const { data } = await em.mutator(users).insertOne({ username: "user1" });
|
||||
const res = await em.mutator(posts).insertOne({
|
||||
title: "post1",
|
||||
users_id: data.id
|
||||
users_id: data.id,
|
||||
});
|
||||
expect(res.data.users_id).toBe(data.id);
|
||||
|
||||
// setting "null" should be allowed
|
||||
const res2 = await em.mutator(posts).insertOne({
|
||||
title: "post1",
|
||||
users_id: null
|
||||
users_id: null,
|
||||
});
|
||||
expect(res2.data.users_id).toBe(null);
|
||||
});
|
||||
@@ -135,14 +136,14 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
||||
const { data } = await em.mutator(users).insertOne({ username: "user1" });
|
||||
const res = await em.mutator(posts).insertOne({
|
||||
title: "post1",
|
||||
users: { $set: { id: data.id } }
|
||||
users: { $set: { id: data.id } },
|
||||
});
|
||||
expect(res.data.users_id).toBe(data.id);
|
||||
|
||||
// setting "null" should be allowed
|
||||
const res2 = await em.mutator(posts).insertOne({
|
||||
title: "post1",
|
||||
users: { $set: { id: null } }
|
||||
users: { $set: { id: null } },
|
||||
});
|
||||
expect(res2.data.users_id).toBe(null);
|
||||
});
|
||||
@@ -151,8 +152,8 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
||||
expect(
|
||||
em.mutator(posts).insertOne({
|
||||
title: "test",
|
||||
users: { $create: { username: "test" } }
|
||||
})
|
||||
users: { $create: { username: "test" } },
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
@@ -162,27 +163,27 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
||||
const res2 = await em.mutator(posts).insertOne({ title: "post1" });
|
||||
|
||||
const up1 = await em.mutator(posts).updateOne(res2.data.id, {
|
||||
users: { $set: { id: res1.data.id } }
|
||||
users: { $set: { id: res1.data.id } },
|
||||
});
|
||||
expect(up1.data.users_id).toBe(res1.data.id);
|
||||
|
||||
const up2 = await em.mutator(posts).updateOne(res2.data.id, {
|
||||
users: { $set: { id: res1_1.data.id } }
|
||||
users: { $set: { id: res1_1.data.id } },
|
||||
});
|
||||
expect(up2.data.users_id).toBe(res1_1.data.id);
|
||||
|
||||
const up3_1 = await em.mutator(posts).updateOne(res2.data.id, {
|
||||
users_id: res1.data.id
|
||||
users_id: res1.data.id,
|
||||
});
|
||||
expect(up3_1.data.users_id).toBe(res1.data.id);
|
||||
|
||||
const up3_2 = await em.mutator(posts).updateOne(res2.data.id, {
|
||||
users_id: res1_1.data.id
|
||||
users_id: res1_1.data.id,
|
||||
});
|
||||
expect(up3_2.data.users_id).toBe(res1_1.data.id);
|
||||
|
||||
const up4 = await em.mutator(posts).updateOne(res2.data.id, {
|
||||
users_id: null
|
||||
users_id: null,
|
||||
});
|
||||
expect(up4.data.users_id).toBe(null);
|
||||
});
|
||||
@@ -199,8 +200,8 @@ describe("[data] Mutator (OneToOne)", async () => {
|
||||
expect(
|
||||
em.mutator(users).insertOne({
|
||||
username: "test",
|
||||
settings_id: 1 // todo: throws because it doesn't exist, but it shouldn't be allowed
|
||||
})
|
||||
settings_id: 1, // todo: throws because it doesn't exist, but it shouldn't be allowed
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
@@ -210,15 +211,15 @@ describe("[data] Mutator (OneToOne)", async () => {
|
||||
expect(
|
||||
em.mutator(users).insertOne({
|
||||
username: "test",
|
||||
settings: { $set: { id: data.id } }
|
||||
})
|
||||
settings: { $set: { id: data.id } },
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
test("insertOne: using $create", async () => {
|
||||
const res = await em.mutator(users).insertOne({
|
||||
username: "test",
|
||||
settings: { $create: { theme: "dark" } }
|
||||
settings: { $create: { theme: "dark" } },
|
||||
});
|
||||
expect(res.data.settings_id).toBeDefined();
|
||||
});
|
||||
@@ -288,14 +289,17 @@ describe("[data] Mutator (Events)", async () => {
|
||||
|
||||
test("events were fired", async () => {
|
||||
const { data } = await mutator.insertOne({ label: "test" });
|
||||
await mutator.emgr.executeAsyncs();
|
||||
expect(events.has(MutatorEvents.MutatorInsertBefore.slug)).toBeTrue();
|
||||
expect(events.has(MutatorEvents.MutatorInsertAfter.slug)).toBeTrue();
|
||||
|
||||
await mutator.updateOne(data.id, { label: "test2" });
|
||||
await mutator.emgr.executeAsyncs();
|
||||
expect(events.has(MutatorEvents.MutatorUpdateBefore.slug)).toBeTrue();
|
||||
expect(events.has(MutatorEvents.MutatorUpdateAfter.slug)).toBeTrue();
|
||||
|
||||
await mutator.deleteOne(data.id);
|
||||
await mutator.emgr.executeAsyncs();
|
||||
expect(events.has(MutatorEvents.MutatorDeleteBefore.slug)).toBeTrue();
|
||||
expect(events.has(MutatorEvents.MutatorDeleteAfter.slug)).toBeTrue();
|
||||
});
|
||||
@@ -303,7 +307,7 @@ describe("[data] Mutator (Events)", async () => {
|
||||
test("insertOne event return is respected", async () => {
|
||||
const posts = proto.entity("posts", {
|
||||
title: proto.text(),
|
||||
views: proto.number()
|
||||
views: proto.number(),
|
||||
});
|
||||
|
||||
const conn = getDummyConnection();
|
||||
@@ -318,10 +322,10 @@ describe("[data] Mutator (Events)", async () => {
|
||||
async (event) => {
|
||||
return {
|
||||
...event.params.data,
|
||||
views: 2
|
||||
views: 2,
|
||||
};
|
||||
},
|
||||
"sync"
|
||||
"sync",
|
||||
);
|
||||
|
||||
const mutator = em.mutator("posts");
|
||||
@@ -329,14 +333,14 @@ describe("[data] Mutator (Events)", async () => {
|
||||
expect(result.data).toEqual({
|
||||
id: 1,
|
||||
title: "test",
|
||||
views: 2
|
||||
views: 2,
|
||||
});
|
||||
});
|
||||
|
||||
test("updateOne event return is respected", async () => {
|
||||
const posts = proto.entity("posts", {
|
||||
title: proto.text(),
|
||||
views: proto.number()
|
||||
views: proto.number(),
|
||||
});
|
||||
|
||||
const conn = getDummyConnection();
|
||||
@@ -351,10 +355,10 @@ describe("[data] Mutator (Events)", async () => {
|
||||
async (event) => {
|
||||
return {
|
||||
...event.params.data,
|
||||
views: event.params.data.views + 1
|
||||
views: event.params.data.views + 1,
|
||||
};
|
||||
},
|
||||
"sync"
|
||||
"sync",
|
||||
);
|
||||
|
||||
const mutator = em.mutator("posts");
|
||||
@@ -363,7 +367,7 @@ describe("[data] Mutator (Events)", async () => {
|
||||
expect(result.data).toEqual({
|
||||
id: 1,
|
||||
title: "test",
|
||||
views: 3
|
||||
views: 3,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import type { Kysely, Transaction } from "kysely";
|
||||
import { Perf } from "../../../src/core/utils";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
LibsqlConnection,
|
||||
ManyToOneRelation,
|
||||
RepositoryEvents,
|
||||
TextField
|
||||
} from "../../../src/data";
|
||||
import { TextField } from "data/fields";
|
||||
import { em as $em, entity as $entity, text as $text } from "data/prototype";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { ManyToOneRelation } from "data/relations";
|
||||
import { RepositoryEvents } from "data/events";
|
||||
import { getDummyConnection } from "../helper";
|
||||
|
||||
type E = Kysely<any> | Transaction<any>;
|
||||
@@ -23,120 +19,6 @@ async function sleep(ms: number) {
|
||||
}
|
||||
|
||||
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 () => {
|
||||
const items = new Entity("items", [new TextField("label")]);
|
||||
const em = new EntityManager([items], dummyConnection);
|
||||
@@ -157,25 +39,88 @@ describe("[Repository]", async () => {
|
||||
// count all
|
||||
const res = await em.repository(items).count();
|
||||
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
|
||||
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.parameters).toEqual(["a", "b"]);
|
||||
expect(res2.count).toBe(2);
|
||||
expect(res2.data.count).toBe(2);
|
||||
|
||||
// check exists
|
||||
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" });
|
||||
expect(res4.exists).toBe(false);
|
||||
expect(res4.data.exists).toBe(false);
|
||||
|
||||
// for now, allow empty filter
|
||||
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]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -183,7 +128,7 @@ describe("[data] Repository (Events)", async () => {
|
||||
const items = new Entity("items", [new TextField("label")]);
|
||||
const categories = new Entity("categories", [new TextField("label")]);
|
||||
const em = new EntityManager([items, categories], dummyConnection, [
|
||||
new ManyToOneRelation(categories, items)
|
||||
new ManyToOneRelation(categories, items),
|
||||
]);
|
||||
await em.schema().sync({ force: true });
|
||||
const events = new Map<string, any>();
|
||||
@@ -198,24 +143,36 @@ describe("[data] Repository (Events)", 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.RepositoryFindOneAfter.slug)).toBeTrue();
|
||||
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.RepositoryFindOneAfter.slug)).toBeTrue();
|
||||
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.RepositoryFindManyAfter.slug)).toBeTrue();
|
||||
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.RepositoryFindManyAfter.slug)).toBeTrue();
|
||||
events.clear();
|
||||
|
||||
// check find one on findMany with limit 1
|
||||
await repo.findMany({ where: { id: 1 }, limit: 1 });
|
||||
await repo.emgr.executeAsyncs();
|
||||
expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue();
|
||||
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
|
||||
events.clear();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { randomString } from "../../../src/core/utils";
|
||||
import { Entity, EntityIndex, EntityManager, SchemaManager, TextField } from "../../../src/data";
|
||||
import { randomString } from "core/utils";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { TextField, EntityIndex } from "data/fields";
|
||||
import { SchemaManager } from "data/schema/SchemaManager";
|
||||
import { getDummyConnection } from "../helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
@@ -15,7 +17,7 @@ describe("SchemaManager tests", async () => {
|
||||
const em = new EntityManager([entity], dummyConnection, [], [index]);
|
||||
const schema = new SchemaManager(em);
|
||||
|
||||
const introspection = schema.getIntrospectionFromEntity(em.entities[0]);
|
||||
const introspection = schema.getIntrospectionFromEntity(em.entities[0]!);
|
||||
expect(introspection).toEqual({
|
||||
name: "test",
|
||||
isView: false,
|
||||
@@ -26,7 +28,7 @@ describe("SchemaManager tests", async () => {
|
||||
isNullable: true,
|
||||
isAutoIncrementing: true,
|
||||
hasDefaultValue: false,
|
||||
comment: undefined
|
||||
comment: undefined,
|
||||
},
|
||||
{
|
||||
name: "username",
|
||||
@@ -34,7 +36,7 @@ describe("SchemaManager tests", async () => {
|
||||
isNullable: true,
|
||||
isAutoIncrementing: false,
|
||||
hasDefaultValue: false,
|
||||
comment: undefined
|
||||
comment: undefined,
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
@@ -42,7 +44,7 @@ describe("SchemaManager tests", async () => {
|
||||
isNullable: true,
|
||||
isAutoIncrementing: false,
|
||||
hasDefaultValue: false,
|
||||
comment: undefined
|
||||
comment: undefined,
|
||||
},
|
||||
{
|
||||
name: "bio",
|
||||
@@ -50,8 +52,8 @@ describe("SchemaManager tests", async () => {
|
||||
isNullable: true,
|
||||
isAutoIncrementing: false,
|
||||
hasDefaultValue: false,
|
||||
comment: undefined
|
||||
}
|
||||
comment: undefined,
|
||||
},
|
||||
],
|
||||
indices: [
|
||||
{
|
||||
@@ -61,11 +63,11 @@ describe("SchemaManager tests", async () => {
|
||||
columns: [
|
||||
{
|
||||
name: "email",
|
||||
order: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
order: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -77,10 +79,10 @@ describe("SchemaManager tests", async () => {
|
||||
new Entity(table, [
|
||||
new TextField("username"),
|
||||
new TextField("email"),
|
||||
new TextField("bio")
|
||||
])
|
||||
new TextField("bio"),
|
||||
]),
|
||||
],
|
||||
dummyConnection
|
||||
dummyConnection,
|
||||
);
|
||||
const kysely = em.connection.kysely;
|
||||
|
||||
@@ -101,15 +103,15 @@ describe("SchemaManager tests", async () => {
|
||||
name: table,
|
||||
isNew: false,
|
||||
columns: { add: ["bio"], drop: [], change: [] },
|
||||
indices: { add: [], drop: [index] }
|
||||
}
|
||||
indices: { add: [], drop: [index] },
|
||||
},
|
||||
]);
|
||||
|
||||
// now sync
|
||||
await schema.sync({ force: true, drop: true });
|
||||
const diffAfter = await schema.getDiff();
|
||||
|
||||
console.log("diffAfter", diffAfter);
|
||||
//console.log("diffAfter", diffAfter);
|
||||
expect(diffAfter.length).toBe(0);
|
||||
|
||||
await kysely.schema.dropTable(table).execute();
|
||||
@@ -119,7 +121,7 @@ describe("SchemaManager tests", async () => {
|
||||
const table = "drop_column";
|
||||
const em = new EntityManager(
|
||||
[new Entity(table, [new TextField("username")])],
|
||||
dummyConnection
|
||||
dummyConnection,
|
||||
);
|
||||
const kysely = em.connection.kysely;
|
||||
|
||||
@@ -141,10 +143,10 @@ describe("SchemaManager tests", async () => {
|
||||
columns: {
|
||||
add: [],
|
||||
drop: ["email"],
|
||||
change: []
|
||||
change: [],
|
||||
},
|
||||
indices: { add: [], drop: [] }
|
||||
}
|
||||
indices: { add: [], drop: [] },
|
||||
},
|
||||
]);
|
||||
|
||||
// now sync
|
||||
@@ -165,15 +167,15 @@ describe("SchemaManager tests", async () => {
|
||||
new Entity(usersTable, [
|
||||
new TextField("username"),
|
||||
new TextField("email"),
|
||||
new TextField("bio")
|
||||
new TextField("bio"),
|
||||
]),
|
||||
new Entity(postsTable, [
|
||||
new TextField("title"),
|
||||
new TextField("content"),
|
||||
new TextField("created_at")
|
||||
])
|
||||
new TextField("created_at"),
|
||||
]),
|
||||
],
|
||||
dummyConnection
|
||||
dummyConnection,
|
||||
);
|
||||
const kysely = em.connection.kysely;
|
||||
|
||||
@@ -192,7 +194,7 @@ describe("SchemaManager tests", async () => {
|
||||
name: usersTable,
|
||||
isNew: false,
|
||||
columns: { add: ["bio"], drop: [], change: [] },
|
||||
indices: { add: [], drop: [] }
|
||||
indices: { add: [], drop: [] },
|
||||
},
|
||||
{
|
||||
name: postsTable,
|
||||
@@ -200,10 +202,10 @@ describe("SchemaManager tests", async () => {
|
||||
columns: {
|
||||
add: ["id", "title", "content", "created_at"],
|
||||
drop: [],
|
||||
change: []
|
||||
change: [],
|
||||
},
|
||||
indices: { add: [], drop: [] }
|
||||
}
|
||||
indices: { add: [], drop: [] },
|
||||
},
|
||||
]);
|
||||
|
||||
// now sync
|
||||
@@ -228,8 +230,8 @@ describe("SchemaManager tests", async () => {
|
||||
name: entity.name,
|
||||
isNew: true,
|
||||
columns: { add: ["id", "email"], drop: [], change: [] },
|
||||
indices: { add: [index.name!], drop: [] }
|
||||
}
|
||||
indices: { add: [index.name!], drop: [] },
|
||||
},
|
||||
]);
|
||||
|
||||
// sync and then check again
|
||||
@@ -256,8 +258,8 @@ describe("SchemaManager tests", async () => {
|
||||
name: entity.name,
|
||||
isNew: false,
|
||||
columns: { add: [], drop: [], change: [] },
|
||||
indices: { add: [index.name!], drop: [] }
|
||||
}
|
||||
indices: { add: [index.name!], drop: [] },
|
||||
},
|
||||
]);
|
||||
|
||||
// sync and then check again
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { getDummyConnection } from "../helper";
|
||||
import { WhereBuilder, type WhereQuery } from "data/entities/query/WhereBuilder";
|
||||
|
||||
function qb() {
|
||||
const c = getDummyConnection();
|
||||
const kysely = c.dummyConnection.kysely;
|
||||
return kysely.selectFrom("t").selectAll();
|
||||
}
|
||||
function compile(q: WhereQuery) {
|
||||
const { sql, parameters } = WhereBuilder.addClause(qb(), q).compile();
|
||||
return { sql, parameters };
|
||||
}
|
||||
|
||||
describe("WhereBuilder", () => {
|
||||
test("single validation", () => {
|
||||
const tests: [WhereQuery, string, any[]][] = [
|
||||
[{ name: "Michael", age: 40 }, '("name" = ? and "age" = ?)', ["Michael", 40]],
|
||||
[{ name: { $eq: "Michael" } }, '"name" = ?', ["Michael"]],
|
||||
[{ int: { $between: [1, 2] } }, '"int" between ? and ?', [1, 2]],
|
||||
[{ val: { $isnull: 1 } }, '"val" is null', []],
|
||||
[{ val: { $isnull: true } }, '"val" is null', []],
|
||||
[{ val: { $isnull: 0 } }, '"val" is not null', []],
|
||||
[{ val: { $isnull: false } }, '"val" is not null', []],
|
||||
[{ val: { $like: "what" } }, '"val" like ?', ["what"]],
|
||||
[{ val: { $like: "w*t" } }, '"val" like ?', ["w%t"]],
|
||||
];
|
||||
|
||||
for (const [query, expectedSql, expectedParams] of tests) {
|
||||
const { sql, parameters } = compile(query);
|
||||
expect(sql).toContain(`select * from "t" where ${expectedSql}`);
|
||||
expect(parameters).toEqual(expectedParams);
|
||||
}
|
||||
});
|
||||
|
||||
test("multiple validations", () => {
|
||||
const tests: [WhereQuery, string, any[]][] = [
|
||||
// multiple constraints per property
|
||||
[{ val: { $lt: 10, $gte: 3 } }, '("val" < ? and "val" >= ?)', [10, 3]],
|
||||
[{ val: { $lt: 10, $gte: 3 } }, '("val" < ? and "val" >= ?)', [10, 3]],
|
||||
[{ val: { $lt: 10, $gte: 3 } }, '("val" < ? and "val" >= ?)', [10, 3]],
|
||||
|
||||
// multiple properties
|
||||
[
|
||||
{ val1: { $eq: "foo" }, val2: { $eq: "bar" } },
|
||||
'("val1" = ? and "val2" = ?)',
|
||||
["foo", "bar"],
|
||||
],
|
||||
[
|
||||
{ val1: { $eq: "foo" }, val2: { $eq: "bar" } },
|
||||
'("val1" = ? and "val2" = ?)',
|
||||
["foo", "bar"],
|
||||
],
|
||||
|
||||
// or constructs
|
||||
[
|
||||
{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } },
|
||||
'("val1" = ? or "val2" = ?)',
|
||||
["foo", "bar"],
|
||||
],
|
||||
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, '("val1" = ? or "val1" = ?)', [1, 2]],
|
||||
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, '("val1" = ? or "val1" = ?)', [1, 2]],
|
||||
];
|
||||
|
||||
for (const [query, expectedSql, expectedParams] of tests) {
|
||||
const { sql, parameters } = compile(query);
|
||||
expect(sql).toContain(`select * from "t" where ${expectedSql}`);
|
||||
expect(parameters).toEqual(expectedParams);
|
||||
}
|
||||
});
|
||||
|
||||
test("keys", () => {
|
||||
const tests: [WhereQuery, string[]][] = [
|
||||
// multiple constraints per property
|
||||
[{ val: { $lt: 10, $gte: 3 } }, ["val"]],
|
||||
|
||||
// multiple properties
|
||||
[{ val1: { $eq: "foo" }, val2: { $eq: "bar" } }, ["val1", "val2"]],
|
||||
|
||||
// or constructs
|
||||
[{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } }, ["val1", "val2"]],
|
||||
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, ["val1"]],
|
||||
];
|
||||
|
||||
for (const [query, expectedKeys] of tests) {
|
||||
const keys = WhereBuilder.getPropertyNames(query);
|
||||
expect(keys).toEqual(expectedKeys);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,10 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { _jsonp } from "../../../src/core/utils";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
ManyToManyRelation,
|
||||
ManyToOneRelation,
|
||||
PolymorphicRelation,
|
||||
TextField,
|
||||
WithBuilder
|
||||
} from "../../../src/data";
|
||||
import * as proto from "../../../src/data/prototype";
|
||||
import { compileQb, prettyPrintQb, schemaToEm } from "../../helper";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { ManyToManyRelation, ManyToOneRelation, PolymorphicRelation } from "data/relations";
|
||||
import { TextField } from "data/fields";
|
||||
import * as proto from "data/prototype";
|
||||
import { WithBuilder } from "data/entities/query/WithBuilder";
|
||||
import { schemaToEm } from "../../helper";
|
||||
import { getDummyConnection } from "../helper";
|
||||
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
@@ -21,32 +15,32 @@ describe("[data] WithBuilder", async () => {
|
||||
{
|
||||
posts: proto.entity("posts", {}),
|
||||
users: proto.entity("users", {}),
|
||||
media: proto.entity("media", {})
|
||||
media: proto.entity("media", {}),
|
||||
},
|
||||
({ relation }, { posts, users, media }) => {
|
||||
relation(posts).manyToOne(users);
|
||||
relation(users).polyToOne(media, { mappedBy: "avatar" });
|
||||
}
|
||||
},
|
||||
);
|
||||
const em = schemaToEm(schema);
|
||||
|
||||
expect(WithBuilder.validateWiths(em, "posts", undefined)).toBe(0);
|
||||
expect(WithBuilder.validateWiths(em, "posts", undefined as any)).toBe(0);
|
||||
expect(WithBuilder.validateWiths(em, "posts", {})).toBe(0);
|
||||
expect(WithBuilder.validateWiths(em, "posts", { users: {} })).toBe(1);
|
||||
expect(
|
||||
WithBuilder.validateWiths(em, "posts", {
|
||||
users: {
|
||||
with: { avatar: {} }
|
||||
}
|
||||
})
|
||||
with: { avatar: {} },
|
||||
},
|
||||
}),
|
||||
).toBe(2);
|
||||
expect(() => WithBuilder.validateWiths(em, "posts", { author: {} })).toThrow();
|
||||
expect(() =>
|
||||
WithBuilder.validateWiths(em, "posts", {
|
||||
users: {
|
||||
with: { glibberish: {} }
|
||||
}
|
||||
})
|
||||
with: { glibberish: {} },
|
||||
},
|
||||
}),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
@@ -56,8 +50,8 @@ describe("[data] WithBuilder", async () => {
|
||||
|
||||
expect(() =>
|
||||
WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, {
|
||||
posts: {}
|
||||
})
|
||||
posts: {},
|
||||
}),
|
||||
).toThrow('Relation "users<>posts" not found');
|
||||
});
|
||||
|
||||
@@ -68,13 +62,13 @@ describe("[data] WithBuilder", async () => {
|
||||
const em = new EntityManager([users, posts], dummyConnection, relations);
|
||||
|
||||
const qb = WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, {
|
||||
posts: {}
|
||||
posts: {},
|
||||
});
|
||||
|
||||
const res = qb.compile();
|
||||
|
||||
expect(res.sql).toBe(
|
||||
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'content\', "agg"."content", \'author_id\', "agg"."author_id")), \'[]\') from (select "posts"."id" as "id", "posts"."content" as "content", "posts"."author_id" as "author_id" from "posts" as "posts" where "posts"."author_id" = "users"."id" order by "posts"."id" asc limit ? offset ?) as agg) as "posts" from "users"'
|
||||
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'content\', "agg"."content", \'author_id\', "agg"."author_id")), \'[]\') from (select "posts"."id" as "id", "posts"."content" as "content", "posts"."author_id" as "author_id" from "posts" as "posts" where "posts"."author_id" = "users"."id" order by "posts"."id" asc limit ? offset ?) as agg) as "posts" from "users"',
|
||||
);
|
||||
expect(res.parameters).toEqual([10, 0]);
|
||||
|
||||
@@ -83,16 +77,16 @@ describe("[data] WithBuilder", async () => {
|
||||
em.connection.kysely.selectFrom("posts"),
|
||||
posts, // @todo: try with "users", it gives output!
|
||||
{
|
||||
author: {}
|
||||
}
|
||||
author: {},
|
||||
},
|
||||
);
|
||||
|
||||
const res2 = qb2.compile();
|
||||
|
||||
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 () => {
|
||||
@@ -124,7 +118,7 @@ describe("[data] WithBuilder", async () => {
|
||||
.values([
|
||||
{ posts_id: 1, categories_id: 1 },
|
||||
{ posts_id: 2, categories_id: 2 },
|
||||
{ posts_id: 1, categories_id: 2 }
|
||||
{ posts_id: 1, categories_id: 2 },
|
||||
])
|
||||
.execute();
|
||||
|
||||
@@ -138,14 +132,14 @@ describe("[data] WithBuilder", async () => {
|
||||
title: "fashion post",
|
||||
categories: [
|
||||
{ id: 1, label: "fashion" },
|
||||
{ id: 2, label: "beauty" }
|
||||
]
|
||||
{ id: 2, label: "beauty" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "beauty post",
|
||||
categories: [{ id: 2, label: "beauty" }]
|
||||
}
|
||||
categories: [{ id: 2, label: "beauty" }],
|
||||
},
|
||||
]);
|
||||
|
||||
const res2 = await em.repository(categories).findMany({ with: { posts: {} } });
|
||||
@@ -156,21 +150,21 @@ describe("[data] WithBuilder", async () => {
|
||||
{
|
||||
id: 1,
|
||||
label: "fashion",
|
||||
posts: [{ id: 1, title: "fashion post" }]
|
||||
posts: [{ id: 1, title: "fashion post" }],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: "beauty",
|
||||
posts: [
|
||||
{ id: 1, title: "fashion post" },
|
||||
{ id: 2, title: "beauty post" }
|
||||
]
|
||||
{ id: 2, title: "beauty post" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label: "tech",
|
||||
posts: []
|
||||
}
|
||||
posts: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -181,7 +175,7 @@ describe("[data] WithBuilder", async () => {
|
||||
const entities = [media, categories];
|
||||
const single = new PolymorphicRelation(categories, media, {
|
||||
mappedBy: "single",
|
||||
targetCardinality: 1
|
||||
targetCardinality: 1,
|
||||
});
|
||||
const multiple = new PolymorphicRelation(categories, media, { mappedBy: "multiple" });
|
||||
|
||||
@@ -191,23 +185,23 @@ describe("[data] WithBuilder", async () => {
|
||||
em,
|
||||
em.connection.kysely.selectFrom("categories"),
|
||||
categories,
|
||||
{ single: {} }
|
||||
{ single: {} },
|
||||
);
|
||||
const res = qb.compile();
|
||||
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(
|
||||
em,
|
||||
em.connection.kysely.selectFrom("categories"),
|
||||
categories,
|
||||
{ multiple: {} }
|
||||
{ multiple: {} },
|
||||
);
|
||||
const res2 = qb2.compile();
|
||||
expect(res2.sql).toBe(
|
||||
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'path\', "agg"."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 agg) as "multiple" from "categories"'
|
||||
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'path\', "agg"."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 agg) as "multiple" from "categories"',
|
||||
);
|
||||
expect(res2.parameters).toEqual(["categories.multiple", 10, 0]);
|
||||
});
|
||||
@@ -240,16 +234,16 @@ describe("[data] WithBuilder", async () => {
|
||||
{
|
||||
posts: proto.entity("posts", {}),
|
||||
users: proto.entity("users", {
|
||||
username: proto.text()
|
||||
username: proto.text(),
|
||||
}),
|
||||
media: proto.entity("media", {
|
||||
path: proto.text()
|
||||
})
|
||||
path: proto.text(),
|
||||
}),
|
||||
},
|
||||
({ relation }, { posts, users, media }) => {
|
||||
relation(posts).manyToOne(users);
|
||||
relation(users).polyToOne(media, { mappedBy: "avatar" });
|
||||
}
|
||||
},
|
||||
);
|
||||
const em = schemaToEm(schema);
|
||||
|
||||
@@ -265,18 +259,18 @@ describe("[data] WithBuilder", async () => {
|
||||
with: {
|
||||
avatar: {
|
||||
select: ["id", "path"],
|
||||
limit: 2 // ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
limit: 2, // ignored
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
//prettyPrintQb(qb);
|
||||
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 () => {
|
||||
@@ -285,17 +279,17 @@ describe("[data] WithBuilder", async () => {
|
||||
posts: proto.entity("posts", {}),
|
||||
comments: proto.entity("comments", {}),
|
||||
users: proto.entity("users", {
|
||||
username: proto.text()
|
||||
username: proto.text(),
|
||||
}),
|
||||
media: proto.entity("media", {
|
||||
path: proto.text()
|
||||
})
|
||||
path: proto.text(),
|
||||
}),
|
||||
},
|
||||
({ relation }, { posts, comments, users, media }) => {
|
||||
relation(posts).manyToOne(users).polyToOne(media, { mappedBy: "images" });
|
||||
relation(users).polyToOne(media, { mappedBy: "avatar" });
|
||||
relation(comments).manyToOne(posts).manyToOne(users);
|
||||
}
|
||||
},
|
||||
);
|
||||
const em = schemaToEm(schema);
|
||||
|
||||
@@ -308,40 +302,40 @@ describe("[data] WithBuilder", async () => {
|
||||
limit: 12,
|
||||
with: {
|
||||
users: {
|
||||
select: ["username"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
select: ["username"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
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 () => {
|
||||
const schema = proto.em(
|
||||
{
|
||||
posts: proto.entity("posts", {
|
||||
title: proto.text()
|
||||
title: proto.text(),
|
||||
}),
|
||||
comments: proto.entity("comments", {
|
||||
content: proto.text()
|
||||
content: proto.text(),
|
||||
}),
|
||||
users: proto.entity("users", {
|
||||
username: proto.text()
|
||||
username: proto.text(),
|
||||
}),
|
||||
media: proto.entity("media", {
|
||||
path: proto.text()
|
||||
})
|
||||
path: proto.text(),
|
||||
}),
|
||||
},
|
||||
({ relation }, { posts, comments, users, media }) => {
|
||||
relation(posts).manyToOne(users).polyToOne(media, { mappedBy: "images" });
|
||||
relation(users).polyToOne(media, { mappedBy: "avatar" });
|
||||
relation(comments).manyToOne(posts).manyToOne(users);
|
||||
}
|
||||
},
|
||||
);
|
||||
const em = schemaToEm(schema);
|
||||
await em.schema().sync({ force: true });
|
||||
@@ -351,7 +345,7 @@ describe("[data] WithBuilder", async () => {
|
||||
await em.mutator("posts").insertMany([
|
||||
{ title: "post1", users_id: 1 },
|
||||
{ title: "post2", users_id: 1 },
|
||||
{ title: "post3", users_id: 2 }
|
||||
{ title: "post3", users_id: 2 },
|
||||
]);
|
||||
await em.mutator("comments").insertMany([
|
||||
{ content: "comment1", posts_id: 1, users_id: 1 },
|
||||
@@ -360,7 +354,7 @@ describe("[data] WithBuilder", async () => {
|
||||
{ content: "comment3", posts_id: 2, users_id: 1 },
|
||||
{ content: "comment4", posts_id: 2, users_id: 2 },
|
||||
{ content: "comment5", posts_id: 3, users_id: 1 },
|
||||
{ content: "comment6", posts_id: 3, users_id: 2 }
|
||||
{ content: "comment6", posts_id: 3, users_id: 2 },
|
||||
]);
|
||||
|
||||
const result = await em.repo("posts").findMany({
|
||||
@@ -371,11 +365,11 @@ describe("[data] WithBuilder", async () => {
|
||||
select: ["content"],
|
||||
with: {
|
||||
users: {
|
||||
select: ["username"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
select: ["username"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.data).toEqual([
|
||||
@@ -385,16 +379,16 @@ describe("[data] WithBuilder", async () => {
|
||||
{
|
||||
content: "comment1",
|
||||
users: {
|
||||
username: "user1"
|
||||
}
|
||||
username: "user1",
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "comment1-1",
|
||||
users: {
|
||||
username: "user1"
|
||||
}
|
||||
}
|
||||
]
|
||||
username: "user1",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "post2",
|
||||
@@ -402,16 +396,16 @@ describe("[data] WithBuilder", async () => {
|
||||
{
|
||||
content: "comment3",
|
||||
users: {
|
||||
username: "user1"
|
||||
}
|
||||
username: "user1",
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "comment4",
|
||||
users: {
|
||||
username: "user2"
|
||||
}
|
||||
}
|
||||
]
|
||||
username: "user2",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "post3",
|
||||
@@ -419,17 +413,17 @@ describe("[data] WithBuilder", async () => {
|
||||
{
|
||||
content: "comment5",
|
||||
users: {
|
||||
username: "user1"
|
||||
}
|
||||
username: "user1",
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "comment6",
|
||||
users: {
|
||||
username: "user2"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
username: "user2",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
//console.log(_jsonp(result.data));
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { EntityManager } from "../../../../src/data";
|
||||
import { EntityManager } from "data/entities/EntityManager";
|
||||
import { getDummyConnection } from "../../helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
@@ -22,10 +22,10 @@ describe("Connection", async () => {
|
||||
columns: [
|
||||
{
|
||||
name: "name",
|
||||
order: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
order: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -54,14 +54,14 @@ describe("Connection", async () => {
|
||||
columns: [
|
||||
{
|
||||
name: "name",
|
||||
order: 0
|
||||
order: 0,
|
||||
},
|
||||
{
|
||||
name: "desc",
|
||||
order: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
order: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -83,10 +83,10 @@ describe("Connection", async () => {
|
||||
columns: [
|
||||
{
|
||||
name: "name",
|
||||
order: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
order: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,10 @@
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { BooleanField } from "../../../../src/data";
|
||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
||||
import { BooleanField } from "data/fields";
|
||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||
|
||||
describe("[data] BooleanField", async () => {
|
||||
runBaseFieldTests(BooleanField, { defaultValue: true, schemaType: "boolean" });
|
||||
fieldTestSuite(bunTestRunner, BooleanField, { defaultValue: true, schemaType: "boolean" });
|
||||
|
||||
test("transformRetrieve", async () => {
|
||||
const field = new BooleanField("test");
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { DateField } from "../../../../src/data";
|
||||
import { runBaseFieldTests } from "./inc";
|
||||
import { describe, test } from "bun:test";
|
||||
import { DateField } from "data/fields";
|
||||
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
|
||||
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
|
||||
test("week", async () => {
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { EnumField } from "../../../../src/data";
|
||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
||||
import { EnumField } from "data/fields";
|
||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||
|
||||
function options(strings: string[]) {
|
||||
return { type: "strings", values: strings };
|
||||
}
|
||||
|
||||
describe("[data] EnumField", async () => {
|
||||
runBaseFieldTests(
|
||||
fieldTestSuite(
|
||||
bunTestRunner,
|
||||
// @ts-ignore
|
||||
EnumField,
|
||||
{ defaultValue: "a", schemaType: "text" },
|
||||
{ options: options(["a", "b", "c"]) }
|
||||
{ options: options(["a", "b", "c"]) },
|
||||
);
|
||||
|
||||
test("yields if default value is not a valid option", async () => {
|
||||
expect(
|
||||
() => new EnumField("test", { options: options(["a", "b"]), default_value: "c" })
|
||||
// @ts-ignore
|
||||
() => new EnumField("test", { options: options(["a", "b"]), default_value: "c" }),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
test("transformPersist (config)", async () => {
|
||||
// @ts-ignore
|
||||
const field = new EnumField("test", { options: options(["a", "b", "c"]) });
|
||||
|
||||
expect(transformPersist(field, null)).resolves.toBeUndefined();
|
||||
@@ -29,9 +34,10 @@ describe("[data] EnumField", async () => {
|
||||
|
||||
test("transformRetrieve", async () => {
|
||||
const field = new EnumField("test", {
|
||||
// @ts-ignore
|
||||
options: options(["a", "b", "c"]),
|
||||
default_value: "a",
|
||||
required: true
|
||||
required: true,
|
||||
});
|
||||
|
||||
expect(field.transformRetrieve(null)).toBe("a");
|
||||
|
||||
@@ -1,43 +1,50 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Default, parse, stripMark } from "../../../../src/core/utils";
|
||||
import { Field, type SchemaResponse, TextField, baseFieldConfigSchema } from "../../../../src/data";
|
||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
||||
import { baseFieldConfigSchema, Field } from "../../../../src/data/fields/Field";
|
||||
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import { stripMark } from "core/utils/schema";
|
||||
|
||||
describe("[data] Field", async () => {
|
||||
class FieldSpec extends Field {
|
||||
schema(): SchemaResponse {
|
||||
return this.useSchemaHelper("text");
|
||||
}
|
||||
getSchema() {
|
||||
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 () => {
|
||||
const config = Default(baseFieldConfigSchema, {});
|
||||
expect(stripMark(new FieldSpec("test").config)).toEqual(config);
|
||||
const config = baseFieldConfigSchema.template({});
|
||||
expect(stripMark(new FieldSpec("test").config)).toEqual(config as any);
|
||||
});
|
||||
|
||||
test("transformPersist (specific)", async () => {
|
||||
const required = new FieldSpec("test", { required: true });
|
||||
const requiredDefault = new FieldSpec("test", {
|
||||
required: true,
|
||||
default_value: "test"
|
||||
default_value: "test",
|
||||
});
|
||||
|
||||
expect(required.transformPersist(null, undefined as any, undefined as any)).rejects.toThrow();
|
||||
expect(
|
||||
required.transformPersist(undefined, undefined as any, undefined as any)
|
||||
required.transformPersist(undefined, undefined as any, undefined as any),
|
||||
).rejects.toThrow();
|
||||
|
||||
// works because it has a default value
|
||||
expect(
|
||||
requiredDefault.transformPersist(null, undefined as any, undefined as any)
|
||||
requiredDefault.transformPersist(null, undefined as any, undefined as any),
|
||||
).resolves.toBeDefined();
|
||||
expect(
|
||||
requiredDefault.transformPersist(undefined, undefined as any, undefined as any)
|
||||
requiredDefault.transformPersist(undefined, undefined as any, undefined as any),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Type } from "../../../../src/core/utils";
|
||||
import {
|
||||
Entity,
|
||||
EntityIndex,
|
||||
type EntityManager,
|
||||
Field,
|
||||
type SchemaResponse
|
||||
} from "../../../../src/data";
|
||||
import { Entity } from "data/entities";
|
||||
import { Field, EntityIndex } from "data/fields";
|
||||
import { s } from "core/utils/schema";
|
||||
|
||||
class TestField extends Field {
|
||||
protected getSchema(): any {
|
||||
return Type.Any();
|
||||
return s.any();
|
||||
}
|
||||
|
||||
schema(em: EntityManager<any>): SchemaResponse {
|
||||
override schema() {
|
||||
return undefined as any;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { JsonField } from "../../../../src/data";
|
||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
||||
import { JsonField } from "data/fields";
|
||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||
|
||||
describe("[data] JsonField", async () => {
|
||||
const field = new JsonField("test");
|
||||
runBaseFieldTests(JsonField, {
|
||||
fieldTestSuite(bunTestRunner, JsonField, {
|
||||
defaultValue: { a: 1 },
|
||||
sampleValues: ["string", { test: 1 }, 1],
|
||||
schemaType: "text"
|
||||
schemaType: "text",
|
||||
});
|
||||
|
||||
test("transformPersist (no config)", async () => {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { JsonSchemaField } from "../../../../src/data";
|
||||
import { runBaseFieldTests } from "./inc";
|
||||
import { JsonSchemaField } from "data/fields";
|
||||
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||
|
||||
describe("[data] JsonSchemaField", async () => {
|
||||
runBaseFieldTests(JsonSchemaField, { defaultValue: {}, schemaType: "text" });
|
||||
// @ts-ignore
|
||||
fieldTestSuite({ expect, test }, JsonSchemaField, { defaultValue: {}, schemaType: "text" });
|
||||
|
||||
// @todo: add JsonSchemaField tests
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { NumberField } from "../../../../src/data";
|
||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
||||
import { NumberField } from "data/fields";
|
||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||
|
||||
describe("[data] NumberField", async () => {
|
||||
test("transformPersist (config)", async () => {
|
||||
@@ -15,5 +16,5 @@ describe("[data] NumberField", async () => {
|
||||
expect(transformPersist(field2, 10000)).resolves.toBe(10000);
|
||||
});
|
||||
|
||||
runBaseFieldTests(NumberField, { defaultValue: 12, schemaType: "integer" });
|
||||
fieldTestSuite(bunTestRunner, NumberField, { defaultValue: 12, schemaType: "integer" });
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { PrimaryField } from "../../../../src/data";
|
||||
import { PrimaryField } from "data/fields";
|
||||
|
||||
describe("[data] PrimaryField", async () => {
|
||||
const field = new PrimaryField("primary");
|
||||
@@ -10,7 +10,12 @@ describe("[data] PrimaryField", async () => {
|
||||
|
||||
test("schema", () => {
|
||||
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 () => {
|
||||
@@ -34,4 +39,28 @@ describe("[data] PrimaryField", async () => {
|
||||
expect(field.transformPersist(1)).rejects.toThrow();
|
||||
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 { TextField } from "../../../../src/data";
|
||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
||||
import { TextField } from "data/fields";
|
||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
|
||||
describe("[data] TextField", async () => {
|
||||
test("transformPersist (config)", async () => {
|
||||
@@ -11,5 +12,5 @@ describe("[data] TextField", async () => {
|
||||
expect(transformPersist(field, "abc")).resolves.toBe("abc");
|
||||
});
|
||||
|
||||
runBaseFieldTests(TextField, { defaultValue: "abc", schemaType: "text" });
|
||||
fieldTestSuite(bunTestRunner, TextField, { defaultValue: "abc", schemaType: "text" });
|
||||
});
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { describe, expect, it, test } from "bun:test";
|
||||
import { Entity, type EntityManager } from "../../../../src/data";
|
||||
import { Entity, type EntityManager } from "data/entities";
|
||||
import {
|
||||
type BaseRelationConfig,
|
||||
EntityRelation,
|
||||
EntityRelationAnchor,
|
||||
RelationTypes
|
||||
} from "../../../../src/data/relations";
|
||||
RelationTypes,
|
||||
} from "data/relations";
|
||||
|
||||
class TestEntityRelation extends EntityRelation {
|
||||
constructor(config?: BaseRelationConfig) {
|
||||
super(
|
||||
new EntityRelationAnchor(new Entity("source"), "source"),
|
||||
new EntityRelationAnchor(new Entity("target"), "target"),
|
||||
config
|
||||
config,
|
||||
);
|
||||
}
|
||||
initialize(em: EntityManager<any>) {}
|
||||
@@ -24,11 +24,11 @@ class TestEntityRelation extends EntityRelation {
|
||||
return this;
|
||||
}
|
||||
|
||||
buildWith(a: any, b: any, c: any): any {
|
||||
buildWith(): any {
|
||||
return;
|
||||
}
|
||||
|
||||
buildJoin(a: any, b: any): any {
|
||||
buildJoin(): any {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,23 +30,23 @@ beforeAll(() =>
|
||||
method: init?.method ?? "GET",
|
||||
// @ts-ignore
|
||||
headers: Object.fromEntries(init?.headers?.entries() ?? []),
|
||||
body: init?.body
|
||||
body: init?.body,
|
||||
};
|
||||
|
||||
return new Response(JSON.stringify({ todos: [1, 2], request }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
})
|
||||
}),
|
||||
);
|
||||
afterAll(unmockFetch);
|
||||
|
||||
describe("FetchTask", async () => {
|
||||
describe.skip("FetchTask", async () => {
|
||||
test("Simple fetch", async () => {
|
||||
const task = new FetchTask("Fetch Something", {
|
||||
url: "https://jsonplaceholder.typicode.com/todos/1",
|
||||
method: "GET",
|
||||
headers: [{ key: "Content-Type", value: "application/json" }]
|
||||
headers: [{ key: "Content-Type", value: "application/json" }],
|
||||
});
|
||||
|
||||
const result = await task.run();
|
||||
@@ -62,18 +62,18 @@ describe("FetchTask", async () => {
|
||||
|
||||
expect(
|
||||
// // @ts-expect-error
|
||||
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: 1 })
|
||||
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: 1 }),
|
||||
).toThrow();
|
||||
|
||||
expect(
|
||||
new FetchTask("", {
|
||||
url: "https://jsonplaceholder.typicode.com",
|
||||
method: "invalid"
|
||||
}).execute()
|
||||
method: "invalid",
|
||||
}).execute(),
|
||||
).rejects.toThrow(/^Invalid method/);
|
||||
|
||||
expect(
|
||||
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: "GET" })
|
||||
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: "GET" }),
|
||||
).toBeDefined();
|
||||
|
||||
expect(() => new FetchTask("", { url: "", method: "Invalid" })).toThrow();
|
||||
@@ -85,17 +85,17 @@ describe("FetchTask", async () => {
|
||||
method: "{{ flow.output.method }}",
|
||||
headers: [
|
||||
{ key: "Content-{{ flow.output.headerKey }}", value: "application/json" },
|
||||
{ key: "Authorization", value: "Bearer {{ flow.output.apiKey }}" }
|
||||
{ key: "Authorization", value: "Bearer {{ flow.output.apiKey }}" },
|
||||
],
|
||||
body: JSON.stringify({
|
||||
email: "{{ flow.output.email }}"
|
||||
})
|
||||
email: "{{ flow.output.email }}",
|
||||
}),
|
||||
});
|
||||
const inputs = {
|
||||
headerKey: "Type",
|
||||
apiKey: 123,
|
||||
email: "what@else.com",
|
||||
method: "PATCH"
|
||||
method: "PATCH",
|
||||
};
|
||||
|
||||
const flow = new Flow("", [task]);
|
||||
|
||||
@@ -1,19 +1,35 @@
|
||||
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 "core/utils/schema";
|
||||
|
||||
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 () => {
|
||||
const subTask = new RenderTask("render", {
|
||||
render: "subflow"
|
||||
render: "subflow",
|
||||
});
|
||||
const subflow = new Flow("subflow", [subTask]);
|
||||
|
||||
const task = new LogTask("log");
|
||||
const task2 = new SubFlowTask("sub", {
|
||||
flow: subflow
|
||||
flow: subflow,
|
||||
});
|
||||
const task3 = new RenderTask("render2", {
|
||||
render: "Subflow output: {{ sub.output }}"
|
||||
render: "Subflow output: {{ sub.output }}",
|
||||
});
|
||||
|
||||
const flow = new Flow("test", [task, task2, task3], []);
|
||||
@@ -22,15 +38,13 @@ describe("SubFlowTask", async () => {
|
||||
|
||||
const execution = flow.createExecution();
|
||||
await execution.start();
|
||||
/*console.log(execution.logs);
|
||||
console.log(execution.getResponse());*/
|
||||
|
||||
expect(execution.getResponse()).toEqual("Subflow output: subflow");
|
||||
});
|
||||
|
||||
test("Simple loop", async () => {
|
||||
const subTask = new RenderTask("render", {
|
||||
render: "run {{ flow.output }}"
|
||||
render: "run {{ flow.output }}",
|
||||
});
|
||||
const subflow = new Flow("subflow", [subTask]);
|
||||
|
||||
@@ -38,10 +52,10 @@ describe("SubFlowTask", async () => {
|
||||
const task2 = new SubFlowTask("sub", {
|
||||
flow: subflow,
|
||||
loop: true,
|
||||
input: [1, 2, 3]
|
||||
input: [1, 2, 3],
|
||||
});
|
||||
const task3 = new RenderTask("render2", {
|
||||
render: `Subflow output: {{ sub.output | join: ", " }}`
|
||||
const task3 = new StringifyTask("stringify", {
|
||||
input: "{{ sub.output }}",
|
||||
});
|
||||
|
||||
const flow = new Flow("test", [task, task2, task3], []);
|
||||
@@ -51,41 +65,6 @@ describe("SubFlowTask", async () => {
|
||||
const execution = flow.createExecution();
|
||||
await execution.start();
|
||||
|
||||
console.log("errors", execution.getErrors());
|
||||
|
||||
/*console.log(execution.logs);
|
||||
console.log(execution.getResponse());*/
|
||||
|
||||
expect(execution.getResponse()).toEqual("Subflow output: run 1, run 2, run 3");
|
||||
});
|
||||
|
||||
test("Simple loop from flow input", async () => {
|
||||
const subTask = new RenderTask("render", {
|
||||
render: "run {{ flow.output }}"
|
||||
});
|
||||
|
||||
const subflow = new Flow("subflow", [subTask]);
|
||||
|
||||
const task = new LogTask("log");
|
||||
const task2 = new SubFlowTask("sub", {
|
||||
flow: subflow,
|
||||
loop: true,
|
||||
input: "{{ flow.output | json }}"
|
||||
});
|
||||
const task3 = new RenderTask("render2", {
|
||||
render: `Subflow output: {{ sub.output | join: ", " }}`
|
||||
});
|
||||
|
||||
const flow = new Flow("test", [task, task2, task3], []);
|
||||
flow.task(task).asInputFor(task2);
|
||||
flow.task(task2).asInputFor(task3);
|
||||
|
||||
const execution = flow.createExecution();
|
||||
await execution.start([4, 5, 6]);
|
||||
|
||||
/*console.log(execution.logs);
|
||||
console.log(execution.getResponse());*/
|
||||
|
||||
expect(execution.getResponse()).toEqual("Subflow output: run 4, run 5, run 6");
|
||||
expect(execution.getResponse()).toEqual('"run 1,run 2,run 3"');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Type } from "../../src/core/utils";
|
||||
import { Task } from "../../src/flows";
|
||||
import { dynamic } from "../../src/flows/tasks/Task";
|
||||
import { s } from "core/utils/schema";
|
||||
|
||||
describe("Task", async () => {
|
||||
describe.skip("Task", async () => {
|
||||
test("resolveParams: template with parse", async () => {
|
||||
const result = await Task.resolveParams(
|
||||
Type.Object({ test: dynamic(Type.Number()) }),
|
||||
s.object({ test: dynamic(s.number()) }),
|
||||
{
|
||||
test: "{{ some.path }}"
|
||||
test: "{{ some.path }}",
|
||||
},
|
||||
{
|
||||
some: {
|
||||
path: 1
|
||||
}
|
||||
}
|
||||
path: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.test).toBe(1);
|
||||
@@ -22,15 +22,15 @@ describe("Task", async () => {
|
||||
|
||||
test("resolveParams: with string", async () => {
|
||||
const result = await Task.resolveParams(
|
||||
Type.Object({ test: Type.String() }),
|
||||
s.object({ test: s.string() }),
|
||||
{
|
||||
test: "{{ some.path }}"
|
||||
test: "{{ some.path }}",
|
||||
},
|
||||
{
|
||||
some: {
|
||||
path: "1/1"
|
||||
}
|
||||
}
|
||||
path: "1/1",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.test).toBe("1/1");
|
||||
@@ -38,75 +38,17 @@ describe("Task", async () => {
|
||||
|
||||
test("resolveParams: with object", async () => {
|
||||
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 }}" },
|
||||
},
|
||||
{
|
||||
some: {
|
||||
path: "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 }}"
|
||||
path: "1/1",
|
||||
},
|
||||
},
|
||||
{
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,11 +7,11 @@ const first = getNamedTask(
|
||||
//throw new Error("Error");
|
||||
return {
|
||||
inner: {
|
||||
result: 2
|
||||
}
|
||||
result: 2,
|
||||
},
|
||||
};
|
||||
},
|
||||
1000
|
||||
1000,
|
||||
);
|
||||
const second = getNamedTask("second (if match)");
|
||||
const third = getNamedTask("third (if error)");
|
||||
|
||||
@@ -11,7 +11,7 @@ class ExecTask extends Task {
|
||||
constructor(
|
||||
name: string,
|
||||
params: any,
|
||||
private fn: () => any
|
||||
private fn: () => any,
|
||||
) {
|
||||
super(name, params);
|
||||
}
|
||||
@@ -54,8 +54,8 @@ export function getNamedTask(name: string, _func?: () => Promise<any>, delay?: n
|
||||
return new ExecTask(
|
||||
name,
|
||||
{
|
||||
delay
|
||||
delay,
|
||||
},
|
||||
func
|
||||
func,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ const first = new LogTask("First", { delay: 1000 });
|
||||
const second = new LogTask("Second", { delay: 1000 });
|
||||
const third = new LogTask("Long Third", { delay: 2500 });
|
||||
const fourth = new FetchTask("Fetch Something", {
|
||||
url: "https://jsonplaceholder.typicode.com/todos/1"
|
||||
url: "https://jsonplaceholder.typicode.com/todos/1",
|
||||
});
|
||||
const fifth = new LogTask("Task 4", { delay: 500 }); // without connection
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Hono } from "hono";
|
||||
import { Event, EventManager } from "../../src/core/events";
|
||||
import { type Static, type StaticDecode, Type, parse } from "../../src/core/utils";
|
||||
import { s, parse } from "core/utils/schema";
|
||||
import { EventTrigger, Flow, HttpTrigger, type InputsMap, Task } from "../../src/flows";
|
||||
import { dynamic } from "../../src/flows/tasks/Task";
|
||||
|
||||
@@ -14,19 +14,19 @@ class Passthrough extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
type OutputIn = Static<typeof OutputParamTask.schema>;
|
||||
type OutputOut = StaticDecode<typeof OutputParamTask.schema>;
|
||||
type OutputIn = s.Static<typeof OutputParamTask.schema>;
|
||||
type OutputOut = s.StaticCoerced<typeof OutputParamTask.schema>;
|
||||
|
||||
class OutputParamTask extends Task<typeof OutputParamTask.schema> {
|
||||
type = "output-param";
|
||||
|
||||
static override schema = Type.Object({
|
||||
static override schema = s.strictObject({
|
||||
number: dynamic(
|
||||
Type.Number({
|
||||
title: "Output number"
|
||||
s.number({
|
||||
title: "Output number",
|
||||
}),
|
||||
Number.parseInt
|
||||
)
|
||||
Number.parseInt,
|
||||
),
|
||||
});
|
||||
|
||||
async execute(inputs: InputsMap) {
|
||||
@@ -43,7 +43,7 @@ class PassthroughFlowInput extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
describe("Flow task inputs", async () => {
|
||||
describe.skip("Flow task inputs", async () => {
|
||||
test("types", async () => {
|
||||
const schema = OutputParamTask.schema;
|
||||
|
||||
@@ -75,7 +75,7 @@ describe("Flow task inputs", async () => {
|
||||
test("output/input", async () => {
|
||||
const task = new OutputParamTask("task1", { number: 111 });
|
||||
const task2 = new OutputParamTask("task2", {
|
||||
number: "{{ task1.output }}"
|
||||
number: "{{ task1.output }}",
|
||||
});
|
||||
|
||||
const flow = new Flow("test", [task, task2]);
|
||||
@@ -94,10 +94,10 @@ describe("Flow task inputs", async () => {
|
||||
|
||||
test("input from flow", async () => {
|
||||
const task = new OutputParamTask("task1", {
|
||||
number: "{{flow.output.someFancyParam}}"
|
||||
number: "{{flow.output.someFancyParam}}",
|
||||
});
|
||||
const task2 = new OutputParamTask("task2", {
|
||||
number: "{{task1.output}}"
|
||||
number: "{{task1.output}}",
|
||||
});
|
||||
|
||||
const flow = new Flow("test", [task, task2]);
|
||||
@@ -126,7 +126,7 @@ describe("Flow task inputs", async () => {
|
||||
const emgr = new EventManager({ EventTriggerClass });
|
||||
|
||||
const task = new OutputParamTask("event", {
|
||||
number: "{{flow.output.number}}"
|
||||
number: "{{flow.output.number}}",
|
||||
});
|
||||
const flow = new Flow(
|
||||
"test",
|
||||
@@ -134,8 +134,8 @@ describe("Flow task inputs", async () => {
|
||||
[],
|
||||
new EventTrigger({
|
||||
event: "test-event",
|
||||
mode: "sync"
|
||||
})
|
||||
mode: "sync",
|
||||
}),
|
||||
);
|
||||
flow.setRespondingTask(task);
|
||||
flow.trigger.register(flow, emgr);
|
||||
@@ -155,8 +155,8 @@ describe("Flow task inputs", async () => {
|
||||
new HttpTrigger({
|
||||
path: "/test",
|
||||
method: "GET",
|
||||
mode: "sync"
|
||||
})
|
||||
mode: "sync",
|
||||
}),
|
||||
);
|
||||
flow.setRespondingTask(task);
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ const flows = {
|
||||
back,
|
||||
fanout,
|
||||
parallel,
|
||||
simpleFetch
|
||||
simpleFetch,
|
||||
};
|
||||
|
||||
const arg = process.argv[2];
|
||||
@@ -32,7 +32,7 @@ const colors = [
|
||||
"#F78F1E", // Saffron
|
||||
"#BD10E0", // Vivid Purple
|
||||
"#50E3C2", // Turquoise
|
||||
"#9013FE" // Grape
|
||||
"#9013FE", // Grape
|
||||
];
|
||||
|
||||
const colorsCache: Record<string, string> = {};
|
||||
@@ -82,7 +82,7 @@ function TerminalFlow({ flow }: { flow: Flow }) {
|
||||
}
|
||||
|
||||
return t;
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -92,7 +92,7 @@ function TerminalFlow({ flow }: { flow: Flow }) {
|
||||
console.log("done", response ? response : "(no response)");
|
||||
console.log(
|
||||
"Executed tasks:",
|
||||
execution.logs.map((l) => l.task.name)
|
||||
execution.logs.map((l) => l.task.name),
|
||||
);
|
||||
console.log("Executed count:", execution.logs.length);
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ class ExecTask extends Task {
|
||||
constructor(
|
||||
name: string,
|
||||
params: any,
|
||||
private fn: () => any
|
||||
private fn: () => any,
|
||||
) {
|
||||
super(name, params);
|
||||
}
|
||||
@@ -30,7 +30,7 @@ class ExecTask extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
describe("Flow trigger", async () => {
|
||||
describe.skip("Flow trigger", async () => {
|
||||
test("manual trigger", async () => {
|
||||
let called = false;
|
||||
|
||||
@@ -60,7 +60,7 @@ describe("Flow trigger", async () => {
|
||||
"test",
|
||||
[task],
|
||||
[],
|
||||
new EventTrigger({ event: "test-event", mode: "sync" })
|
||||
new EventTrigger({ event: "test-event", mode: "sync" }),
|
||||
);
|
||||
|
||||
flow.trigger.register(flow, emgr);
|
||||
@@ -107,8 +107,8 @@ describe("Flow trigger", async () => {
|
||||
new HttpTrigger({
|
||||
path: "/test",
|
||||
method: "GET",
|
||||
mode: "sync"
|
||||
})
|
||||
mode: "sync",
|
||||
}),
|
||||
);
|
||||
|
||||
const hono = new Hono();
|
||||
@@ -123,7 +123,7 @@ describe("Flow trigger", async () => {
|
||||
|
||||
test("http trigger with response", async () => {
|
||||
const task = ExecTask.create("http", () => ({
|
||||
called: true
|
||||
called: true,
|
||||
}));
|
||||
const flow = new Flow(
|
||||
"test",
|
||||
@@ -132,8 +132,8 @@ describe("Flow trigger", async () => {
|
||||
new HttpTrigger({
|
||||
path: "/test",
|
||||
method: "GET",
|
||||
mode: "sync"
|
||||
})
|
||||
mode: "sync",
|
||||
}),
|
||||
);
|
||||
flow.setRespondingTask(task);
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { describe, expect, test } from "bun:test";
|
||||
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 "core/utils/schema";
|
||||
import { Condition, ExecutionEvent, FetchTask, Flow, LogTask, Task } from "../../src/flows";
|
||||
|
||||
/*beforeAll(disableConsoleLog);
|
||||
@@ -10,19 +11,19 @@ afterAll(enableConsoleLog);*/
|
||||
class ExecTask extends Task<typeof ExecTask.schema> {
|
||||
type = "exec";
|
||||
|
||||
static override schema = Type.Object({
|
||||
delay: Type.Number({ default: 10 })
|
||||
static override schema = s.object({
|
||||
delay: s.number({ default: 10 }),
|
||||
});
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
params: Static<typeof ExecTask.schema>,
|
||||
private func: () => Promise<any>
|
||||
params: s.Static<typeof ExecTask.schema>,
|
||||
private func: () => Promise<any>,
|
||||
) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -36,12 +37,12 @@ function getTask(num: number = 0, delay: number = 5) {
|
||||
return new ExecTask(
|
||||
`Task ${num}`,
|
||||
{
|
||||
delay
|
||||
delay,
|
||||
},
|
||||
async () => {
|
||||
//console.log(`[DONE] Task: ${num}`);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
//return new LogTask(`Log ${num}`, { delay });
|
||||
}
|
||||
@@ -56,9 +57,9 @@ function getNamedTask(name: string, _func?: () => Promise<any>, delay?: number)
|
||||
return new ExecTask(
|
||||
name,
|
||||
{
|
||||
delay: delay ?? 0
|
||||
delay: delay ?? 0,
|
||||
},
|
||||
func
|
||||
func,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,7 +78,7 @@ function getObjectDiff(obj1, obj2) {
|
||||
return diff;
|
||||
}
|
||||
|
||||
describe("Flow tests", async () => {
|
||||
describe.skip("Flow tests", async () => {
|
||||
test("Simple single task", async () => {
|
||||
const simple = getTask(0);
|
||||
|
||||
@@ -228,7 +229,7 @@ describe("Flow tests", async () => {
|
||||
back
|
||||
.task(third)
|
||||
.getOutTasks()
|
||||
.map((t) => t.name)
|
||||
.map((t) => t.name),
|
||||
).toEqual(["second", "fourth"]);
|
||||
|
||||
const execution = back.createExecution();
|
||||
@@ -263,7 +264,7 @@ describe("Flow tests", async () => {
|
||||
back
|
||||
.task(third)
|
||||
.getOutTasks()
|
||||
.map((t) => t.name)
|
||||
.map((t) => t.name),
|
||||
).toEqual(["second", "fourth"]);
|
||||
|
||||
const execution = back.createExecution();
|
||||
@@ -324,8 +325,8 @@ describe("Flow tests", async () => {
|
||||
const first = getNamedTask("first", async () => {
|
||||
return {
|
||||
inner: {
|
||||
result: 2
|
||||
}
|
||||
result: 2,
|
||||
},
|
||||
};
|
||||
});
|
||||
const second = getNamedTask("second");
|
||||
@@ -361,7 +362,7 @@ describe("Flow tests", async () => {
|
||||
"[event]",
|
||||
event.isStart() ? "start" : "end",
|
||||
event.task().name,
|
||||
event.isStart() ? undefined : event.succeeded()
|
||||
event.isStart() ? undefined : event.succeeded(),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -389,7 +390,7 @@ describe("Flow tests", async () => {
|
||||
const second = new LogTask("Task 1");
|
||||
const third = new LogTask("Task 2", { delay: 50 });
|
||||
const fourth = new FetchTask("Fetch Something", {
|
||||
url: "https://jsonplaceholder.typicode.com/todos/1"
|
||||
url: "https://jsonplaceholder.typicode.com/todos/1",
|
||||
});
|
||||
const fifth = new LogTask("Task 4"); // without connection
|
||||
|
||||
@@ -405,7 +406,7 @@ describe("Flow tests", async () => {
|
||||
// @todo: fix
|
||||
const deserialized = Flow.fromObject("", original, {
|
||||
fetch: { cls: FetchTask },
|
||||
log: { cls: LogTask }
|
||||
log: { cls: LogTask },
|
||||
} as any);
|
||||
|
||||
const diffdeep = getObjectDiff(original, deserialized.toJSON());
|
||||
@@ -414,7 +415,7 @@ describe("Flow tests", async () => {
|
||||
expect(flow.startTask.name).toEqual(deserialized.startTask.name);
|
||||
expect(flow.respondingTask?.name).toEqual(
|
||||
// @ts-ignore
|
||||
deserialized.respondingTask?.name
|
||||
deserialized.respondingTask?.name,
|
||||
);
|
||||
|
||||
//console.log("--- creating original sequence");
|
||||
|
||||
+55
-6
@@ -2,8 +2,12 @@ import { unlink } from "node:fs/promises";
|
||||
import type { SelectQueryBuilder, SqliteDatabase } from "kysely";
|
||||
import Database from "libsql";
|
||||
import { format as sqlFormat } from "sql-formatter";
|
||||
import { type Connection, EntityManager, SqliteLocalConnection } from "../src/data";
|
||||
import type { em as protoEm } from "../src/data/prototype";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { slugify } from "core/utils/strings";
|
||||
import { type Connection, SqliteLocalConnection } from "data/connection";
|
||||
import { EntityManager } from "data/entities/EntityManager";
|
||||
|
||||
export function getDummyDatabase(memory: boolean = true): {
|
||||
dummyDb: SqliteDatabase;
|
||||
@@ -17,7 +21,7 @@ export function getDummyDatabase(memory: boolean = true): {
|
||||
afterAllCleanup: async () => {
|
||||
if (!memory) await unlink(DB_NAME);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -27,7 +31,7 @@ export function getDummyConnection(memory: boolean = true) {
|
||||
|
||||
return {
|
||||
dummyConnection,
|
||||
afterAllCleanup
|
||||
afterAllCleanup,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,14 +39,15 @@ export function getLocalLibsqlConnection() {
|
||||
return { url: "http://127.0.0.1:8080" };
|
||||
}
|
||||
|
||||
type ConsoleSeverity = "log" | "warn" | "error";
|
||||
type ConsoleSeverity = "debug" | "log" | "warn" | "error";
|
||||
const _oldConsoles = {
|
||||
debug: console.debug,
|
||||
log: console.log,
|
||||
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) => {
|
||||
console[severity] = () => null;
|
||||
});
|
||||
@@ -71,3 +76,47 @@ export function schemaToEm(s: ReturnType<typeof protoEm>, conn?: Connection): En
|
||||
|
||||
export const assetsPath = `${import.meta.dir}/_assets`;
|
||||
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 type { AuthResponse } from "../../src/auth";
|
||||
import { auth } from "../../src/auth/middlewares";
|
||||
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);
|
||||
afterAll(enableConsoleLog);
|
||||
@@ -16,25 +19,25 @@ const roles = {
|
||||
"system.schema.read",
|
||||
"system.access.api",
|
||||
"system.config.read",
|
||||
"data.entity.read"
|
||||
"data.entity.read",
|
||||
],
|
||||
is_default: true
|
||||
is_default: true,
|
||||
},
|
||||
admin: {
|
||||
is_default: true,
|
||||
implicit_allow: true
|
||||
}
|
||||
implicit_allow: true,
|
||||
},
|
||||
},
|
||||
strict: {
|
||||
guest: {
|
||||
permissions: ["system.access.api", "system.config.read", "data.entity.read"],
|
||||
is_default: true
|
||||
is_default: true,
|
||||
},
|
||||
admin: {
|
||||
is_default: true,
|
||||
implicit_allow: true
|
||||
}
|
||||
}
|
||||
implicit_allow: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
const configs = {
|
||||
auth: {
|
||||
@@ -42,31 +45,32 @@ const configs = {
|
||||
entity_name: "users",
|
||||
jwt: {
|
||||
secret: secureRandomString(20),
|
||||
issuer: randomString(10)
|
||||
issuer: randomString(10),
|
||||
},
|
||||
roles: roles.strict,
|
||||
guard: {
|
||||
enabled: true
|
||||
}
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
users: {
|
||||
normal: {
|
||||
email: "normal@bknd.io",
|
||||
password: "12345678"
|
||||
password: "12345678",
|
||||
},
|
||||
admin: {
|
||||
email: "admin@bknd.io",
|
||||
password: "12345678",
|
||||
role: "admin"
|
||||
}
|
||||
}
|
||||
role: "admin",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function createAuthApp() {
|
||||
const app = createApp({
|
||||
connection: dummyConnection,
|
||||
initialConfig: {
|
||||
auth: configs.auth
|
||||
}
|
||||
auth: configs.auth,
|
||||
},
|
||||
});
|
||||
|
||||
app.emgr.onEvent(
|
||||
@@ -75,7 +79,7 @@ function createAuthApp() {
|
||||
await app.createUser(configs.users.normal);
|
||||
await app.createUser(configs.users.admin);
|
||||
},
|
||||
"sync"
|
||||
"sync",
|
||||
);
|
||||
|
||||
return app;
|
||||
@@ -94,14 +98,14 @@ const fns = <Mode extends "cookie" | "token" = "token">(app: App, mode?: Mode) =
|
||||
if (mode === "cookie") {
|
||||
return {
|
||||
cookie: `auth=${token};`,
|
||||
...additional
|
||||
...additional,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: token ? `Bearer ${token}` : "",
|
||||
"Content-Type": "application/json",
|
||||
...additional
|
||||
...additional,
|
||||
};
|
||||
}
|
||||
function body(obj?: Record<string, any>) {
|
||||
@@ -118,12 +122,12 @@ const fns = <Mode extends "cookie" | "token" = "token">(app: App, mode?: Mode) =
|
||||
|
||||
return {
|
||||
login: async (
|
||||
user: any
|
||||
user: any,
|
||||
): Promise<{ res: Response; data: Mode extends "token" ? AuthResponse : string }> => {
|
||||
const res = (await app.server.request("/api/auth/password/login", {
|
||||
method: "POST",
|
||||
headers: headers(),
|
||||
body: body(user)
|
||||
body: body(user),
|
||||
})) as Response;
|
||||
|
||||
const data = mode === "cookie" ? getCookie(res, "auth") : await res.json();
|
||||
@@ -133,10 +137,10 @@ const fns = <Mode extends "cookie" | "token" = "token">(app: App, mode?: Mode) =
|
||||
me: async (token?: string): Promise<Pick<AuthResponse, "user">> => {
|
||||
const res = (await app.server.request("/api/auth/me", {
|
||||
method: "GET",
|
||||
headers: headers(token)
|
||||
headers: headers(token),
|
||||
})) as Response;
|
||||
return await res.json();
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -219,7 +223,7 @@ describe("integration auth", () => {
|
||||
|
||||
app.server.get("/get", auth(), async (c) => {
|
||||
return c.json({
|
||||
user: c.get("auth").user ?? null
|
||||
user: c.get("auth").user ?? null,
|
||||
});
|
||||
});
|
||||
app.server.get("/wait", auth(), async (c) => {
|
||||
@@ -232,7 +236,7 @@ describe("integration auth", () => {
|
||||
expect(me.user.email).toBe(configs.users.normal.email);
|
||||
|
||||
app.server.request("/wait", {
|
||||
headers: { Authorization: `Bearer ${data.token}` }
|
||||
headers: { Authorization: `Bearer ${data.token}` },
|
||||
});
|
||||
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { createApp } from "../../src";
|
||||
import { createApp } from "core/test/utils";
|
||||
import { Api } from "../../src/Api";
|
||||
|
||||
describe("integration config", () => {
|
||||
@@ -8,15 +8,14 @@ describe("integration config", () => {
|
||||
await app.build();
|
||||
const api = new Api({
|
||||
host: "http://localhost",
|
||||
fetcher: app.server.request as typeof fetch
|
||||
fetcher: app.server.request as typeof fetch,
|
||||
});
|
||||
|
||||
// create entity
|
||||
await api.system.addConfig("data", "entities.posts", {
|
||||
name: "posts",
|
||||
config: { sort_field: "id", sort_dir: "asc" },
|
||||
fields: { id: { type: "primary", name: "id" }, asdf: { type: "text" } },
|
||||
type: "regular"
|
||||
fields: { id: { type: "primary" }, asdf: { type: "text" } },
|
||||
type: "regular",
|
||||
});
|
||||
|
||||
expect(app.em.entities.map((e) => e.name)).toContain("posts");
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
/// <reference types="@types/bun" />
|
||||
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { createApp, registries } from "../../src";
|
||||
import { StorageLocalAdapter } from "../../src/adapter/node";
|
||||
import { registries } from "../../src";
|
||||
import { createApp } from "core/test/utils";
|
||||
import { mergeObject, randomString } from "../../src/core/utils";
|
||||
import type { TAppMediaConfig } from "../../src/media/media-schema";
|
||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||
import { assetsPath, assetsTmpPath, disableConsoleLog, enableConsoleLog } from "../helper";
|
||||
|
||||
beforeAll(() => {
|
||||
@@ -22,13 +23,13 @@ async function makeApp(mediaOverride: Partial<TAppMediaConfig> = {}) {
|
||||
adapter: {
|
||||
type: "local",
|
||||
config: {
|
||||
path: assetsTmpPath
|
||||
}
|
||||
}
|
||||
path: assetsTmpPath,
|
||||
},
|
||||
},
|
||||
},
|
||||
mediaOverride
|
||||
)
|
||||
}
|
||||
mediaOverride,
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
await app.build();
|
||||
@@ -50,7 +51,7 @@ describe("MediaController", () => {
|
||||
const name = makeName("png");
|
||||
const res = await app.server.request("/api/media/upload/" + name, {
|
||||
method: "POST",
|
||||
body: file
|
||||
body: file,
|
||||
});
|
||||
const result = (await res.json()) as any;
|
||||
expect(result.name).toBe(name);
|
||||
@@ -70,7 +71,7 @@ describe("MediaController", () => {
|
||||
|
||||
const res = await app.server.request("/api/media/upload/" + name, {
|
||||
method: "POST",
|
||||
body: form
|
||||
body: form,
|
||||
});
|
||||
const result = (await res.json()) as any;
|
||||
expect(result.name).toBe(name);
|
||||
@@ -87,7 +88,7 @@ describe("MediaController", () => {
|
||||
const name = makeName("png");
|
||||
const res = await app.server.request("/api/media/upload/" + name, {
|
||||
method: "POST",
|
||||
body: file
|
||||
body: file,
|
||||
});
|
||||
|
||||
expect(res.status).toBe(413);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
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 { StorageAdapter } from "media/storage/StorageAdapter";
|
||||
|
||||
class TestAdapter implements StorageAdapter {
|
||||
class TestAdapter extends StorageAdapter {
|
||||
files: Record<string, FileBody> = {};
|
||||
|
||||
getName() {
|
||||
@@ -60,8 +61,8 @@ describe("Storage", async () => {
|
||||
|
||||
test("uploads a file", async () => {
|
||||
const {
|
||||
meta: { type, size }
|
||||
} = await storage.uploadFile("hello", "world.txt");
|
||||
meta: { type, size },
|
||||
} = await storage.uploadFile("hello" as any, "world.txt");
|
||||
expect({ type, size }).toEqual({ type: "text/plain", size: 0 });
|
||||
});
|
||||
|
||||
@@ -71,6 +72,7 @@ describe("Storage", async () => {
|
||||
});
|
||||
|
||||
test("events were fired", async () => {
|
||||
await storage.emgr.executeAsyncs();
|
||||
expect(events.has(StorageEvents.FileUploadedEvent.slug)).toBeTrue();
|
||||
expect(events.has(StorageEvents.FileDeletedEvent.slug)).toBeTrue();
|
||||
// @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,61 +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", () => {
|
||||
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,46 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { randomString } from "../../../src/core/utils";
|
||||
import { StorageLocalAdapter } from "../../../src/media/storage/adapters/StorageLocalAdapter";
|
||||
|
||||
describe("StorageLocalAdapter", () => {
|
||||
const adapter = new StorageLocalAdapter({
|
||||
path: `${import.meta.dir}/local`
|
||||
});
|
||||
|
||||
const file = Bun.file(`${import.meta.dir}/icon.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,96 +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;
|
||||
|
||||
describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => {
|
||||
console.log("ALL_TESTS", process.env.ALL_TESTS);
|
||||
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)) {
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,13 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import * as large from "../../src/media/storage/mime-types";
|
||||
import * as tiny from "../../src/media/storage/mime-types-tiny";
|
||||
import { getRandomizedFilename } from "../../src/media/utils";
|
||||
|
||||
describe("media/mime-types", () => {
|
||||
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) {
|
||||
expect(tiny.guess(ext)).toBe(mime);
|
||||
@@ -27,7 +30,7 @@ describe("media/mime-types", () => {
|
||||
exts,
|
||||
ext,
|
||||
expected: ex,
|
||||
actual: large.guessMimeType("." + ext)
|
||||
actual: large.guessMimeType("." + ext),
|
||||
});
|
||||
throw new Error(`Failed for ${ext}`);
|
||||
}
|
||||
@@ -36,19 +39,67 @@ describe("media/mime-types", () => {
|
||||
});
|
||||
|
||||
test("isMimeType", () => {
|
||||
expect(tiny.isMimeType("image/jpeg")).toBe(true);
|
||||
expect(tiny.isMimeType("image/jpeg", ["image/png"])).toBe(true);
|
||||
expect(tiny.isMimeType("image/png", ["image/png"])).toBe(false);
|
||||
expect(tiny.isMimeType("image/png")).toBe(true);
|
||||
expect(tiny.isMimeType("whatever")).toBe(false);
|
||||
expect(tiny.isMimeType("text/tab-separated-values")).toBe(true);
|
||||
const tests = [
|
||||
["image/avif", true],
|
||||
["image/AVIF", true],
|
||||
["image/jpeg", true],
|
||||
["image/jpeg", true, ["image/png"]],
|
||||
["image/png", false, ["image/png"]],
|
||||
["image/png", true],
|
||||
["image/heif", true],
|
||||
["image/heic", true],
|
||||
["image/gif", true],
|
||||
["whatever", false],
|
||||
["text/tab-separated-values", true],
|
||||
["application/zip", true],
|
||||
];
|
||||
|
||||
for (const [mime, expected, exclude] of tests) {
|
||||
expect(
|
||||
tiny.isMimeType(mime, exclude as any),
|
||||
`isMimeType(): ${mime} should be ${expected}`,
|
||||
).toBe(expected as any);
|
||||
}
|
||||
});
|
||||
|
||||
test("extension", () => {
|
||||
expect(tiny.extension("image/png")).toBe("png");
|
||||
expect(tiny.extension("image/jpeg")).toBe("jpeg");
|
||||
expect(tiny.extension("application/zip")).toBe("zip");
|
||||
expect(tiny.extension("text/tab-separated-values")).toBe("tsv");
|
||||
expect(tiny.extension("application/zip")).toBe("zip");
|
||||
const tests = [
|
||||
["image/avif", "avif"],
|
||||
["image/png", "png"],
|
||||
["image/PNG", "png"],
|
||||
["image/jpeg", "jpeg"],
|
||||
["application/zip", "zip"],
|
||||
["text/tab-separated-values", "tsv"],
|
||||
["application/zip", "zip"],
|
||||
] as const;
|
||||
|
||||
for (const [mime, ext] of tests) {
|
||||
expect(tiny.extension(mime), `extension(): ${mime} should be ${ext}`).toBe(ext);
|
||||
}
|
||||
});
|
||||
|
||||
test("getRandomizedFilename", () => {
|
||||
const tests = [
|
||||
["file.txt", "txt"],
|
||||
["file.TXT", "txt"],
|
||||
["image.jpg", "jpg"],
|
||||
["image.avif", "avif"],
|
||||
["image.heic", "heic"],
|
||||
["image.jpeg", "jpeg"],
|
||||
["-473Wx593H-466453554-black-MODEL.jpg", "jpg"],
|
||||
["-473Wx593H-466453554-black-MODEL.avif", "avif"],
|
||||
] as const;
|
||||
|
||||
for (const [filename, ext] of tests) {
|
||||
expect(
|
||||
getRandomizedFilename(filename).split(".").pop(),
|
||||
`getRandomizedFilename(): ${filename} should end with ${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,18 @@
|
||||
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 { em, entity, make, text } from "../../src/data";
|
||||
import { AppAuth, type ModuleBuildContext } from "../../src/modules";
|
||||
import { em, entity, make, text } from "data/prototype";
|
||||
import { AppAuth, type ModuleBuildContext } from "modules";
|
||||
import { disableConsoleLog, enableConsoleLog } from "../helper";
|
||||
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
||||
|
||||
describe("AppAuth", () => {
|
||||
test.only("...", () => {
|
||||
const auth = new AppAuth({});
|
||||
console.log(auth.toJSON());
|
||||
console.log(auth.config);
|
||||
});
|
||||
|
||||
moduleTestSuite(AppAuth);
|
||||
|
||||
let ctx: ModuleBuildContext;
|
||||
@@ -22,7 +28,7 @@ describe("AppAuth", () => {
|
||||
|
||||
const config = auth.toJSON();
|
||||
expect(config.jwt).toBeUndefined();
|
||||
expect(config.strategies.password.config).toBeUndefined();
|
||||
expect(config.strategies?.password?.config).toBeUndefined();
|
||||
});
|
||||
|
||||
test("enabling auth: generate secret", async () => {
|
||||
@@ -42,11 +48,12 @@ describe("AppAuth", () => {
|
||||
const auth = new AppAuth(
|
||||
{
|
||||
enabled: true,
|
||||
// @ts-ignore
|
||||
jwt: {
|
||||
secret: "123456"
|
||||
}
|
||||
secret: "123456",
|
||||
},
|
||||
},
|
||||
ctx
|
||||
ctx,
|
||||
);
|
||||
|
||||
await auth.build();
|
||||
@@ -63,19 +70,78 @@ describe("AppAuth", () => {
|
||||
const res = await app.request("/password/register", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: "some@body.com",
|
||||
password: "123456"
|
||||
})
|
||||
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");
|
||||
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("$");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -85,13 +151,14 @@ describe("AppAuth", () => {
|
||||
auth: {
|
||||
enabled: true,
|
||||
jwt: {
|
||||
secret: "123456"
|
||||
}
|
||||
}
|
||||
}
|
||||
secret: "123456",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await app.build();
|
||||
app.registerAdminController();
|
||||
const spy = spyOn(app.module.auth.authenticator, "requestCookieRefresh");
|
||||
|
||||
// register custom route
|
||||
@@ -101,6 +168,10 @@ describe("AppAuth", () => {
|
||||
await app.server.request("/api/system/ping");
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -109,14 +180,14 @@ describe("AppAuth", () => {
|
||||
initialConfig: {
|
||||
auth: {
|
||||
entity_name: "users",
|
||||
enabled: true
|
||||
enabled: true,
|
||||
},
|
||||
data: em({
|
||||
users: entity("users", {
|
||||
additional: text()
|
||||
})
|
||||
}).toJSON()
|
||||
}
|
||||
additional: text(),
|
||||
}),
|
||||
}).toJSON(),
|
||||
},
|
||||
});
|
||||
|
||||
await app.build();
|
||||
@@ -132,21 +203,21 @@ describe("AppAuth", () => {
|
||||
const app = createApp({
|
||||
initialConfig: {
|
||||
auth: {
|
||||
enabled: true
|
||||
enabled: true,
|
||||
},
|
||||
data: em({
|
||||
users: entity("users", {
|
||||
strategy: text({
|
||||
fillable: true,
|
||||
hidden: false
|
||||
hidden: false,
|
||||
}),
|
||||
strategy_value: text({
|
||||
fillable: true,
|
||||
hidden: false
|
||||
})
|
||||
})
|
||||
}).toJSON()
|
||||
}
|
||||
hidden: false,
|
||||
}),
|
||||
}),
|
||||
}).toJSON(),
|
||||
},
|
||||
});
|
||||
await app.build();
|
||||
|
||||
@@ -157,7 +228,7 @@ describe("AppAuth", () => {
|
||||
const authField = make(name, _authFieldProto as any);
|
||||
const field = users.field(name)!;
|
||||
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 { parse } from "../../src/core/utils";
|
||||
import { beforeEach, describe, expect, test } from "bun:test";
|
||||
import { parse } from "core/utils/schema";
|
||||
import { fieldsSchema } from "../../src/data/data-schema";
|
||||
import { AppData } from "../../src/modules";
|
||||
import { moduleTestSuite } from "./module-test-suite";
|
||||
import { AppData, type ModuleBuildContext } from "../../src/modules";
|
||||
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
||||
import * as proto from "data/prototype";
|
||||
|
||||
describe("AppData", () => {
|
||||
moduleTestSuite(AppData);
|
||||
|
||||
let ctx: ModuleBuildContext;
|
||||
beforeEach(() => {
|
||||
ctx = makeCtx();
|
||||
});
|
||||
|
||||
test("field config construction", () => {
|
||||
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,17 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { createApp, registries } from "../../src";
|
||||
import { em, entity, text } from "../../src/data";
|
||||
import { StorageLocalAdapter } from "../../src/media/storage/adapters/StorageLocalAdapter";
|
||||
import { AppMedia } from "../../src/modules";
|
||||
import { createApp } from "core/test/utils";
|
||||
import { em, entity, text } from "data/prototype";
|
||||
import { registries } from "modules/registries";
|
||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||
import { AppMedia } from "../../src/media/AppMedia";
|
||||
import { moduleTestSuite } from "./module-test-suite";
|
||||
|
||||
describe("AppMedia", () => {
|
||||
test.only("...", () => {
|
||||
const media = new AppMedia();
|
||||
console.log(media.toJSON());
|
||||
});
|
||||
|
||||
moduleTestSuite(AppMedia);
|
||||
|
||||
test("should allow additional fields", async () => {
|
||||
@@ -19,16 +25,16 @@ describe("AppMedia", () => {
|
||||
adapter: {
|
||||
type: "local",
|
||||
config: {
|
||||
path: "./"
|
||||
}
|
||||
}
|
||||
path: "./",
|
||||
},
|
||||
},
|
||||
},
|
||||
data: em({
|
||||
media: entity("media", {
|
||||
additional: text()
|
||||
})
|
||||
}).toJSON()
|
||||
}
|
||||
additional: text(),
|
||||
}),
|
||||
}).toJSON(),
|
||||
},
|
||||
});
|
||||
|
||||
await app.build();
|
||||
@@ -49,7 +55,7 @@ describe("AppMedia", () => {
|
||||
"modified_at",
|
||||
"reference",
|
||||
"entity_id",
|
||||
"metadata"
|
||||
"metadata",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { type TSchema, Type, stripMark } from "../../src/core/utils";
|
||||
import { EntityManager, em, entity, index, text } from "../../src/data";
|
||||
import { s, stripMark } from "core/utils/schema";
|
||||
import { em, entity, index, text } from "data/prototype";
|
||||
import { EntityManager } from "data/entities/EntityManager";
|
||||
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
||||
import { Module } from "../../src/modules/Module";
|
||||
import { ModuleHelper } from "modules/ModuleHelper";
|
||||
|
||||
function createModule<Schema extends TSchema>(schema: Schema) {
|
||||
class TestModule extends Module<typeof schema> {
|
||||
function createModule<Schema extends s.Schema>(schema: Schema) {
|
||||
return class TestModule extends Module<Schema> {
|
||||
getSchema() {
|
||||
return schema;
|
||||
}
|
||||
toJSON() {
|
||||
override toJSON() {
|
||||
return this.config;
|
||||
}
|
||||
useForceParse() {
|
||||
override useForceParse() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return TestModule;
|
||||
};
|
||||
}
|
||||
|
||||
describe("Module", async () => {
|
||||
@@ -25,7 +25,7 @@ describe("Module", async () => {
|
||||
test("listener", async () => {
|
||||
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" });
|
||||
|
||||
await m.schema().set({ a: "test2" });
|
||||
@@ -41,13 +41,13 @@ describe("Module", async () => {
|
||||
describe("db schema", () => {
|
||||
class M extends Module {
|
||||
override getSchema() {
|
||||
return Type.Object({});
|
||||
return s.object({});
|
||||
}
|
||||
|
||||
prt = {
|
||||
ensureEntity: this.ensureEntity.bind(this),
|
||||
ensureIndex: this.ensureIndex.bind(this),
|
||||
ensureSchema: this.ensureSchema.bind(this)
|
||||
ensureEntity: this.ctx.helper.ensureEntity.bind(this.ctx.helper),
|
||||
ensureIndex: this.ctx.helper.ensureIndex.bind(this.ctx.helper),
|
||||
ensureSchema: this.ctx.helper.ensureSchema.bind(this.ctx.helper),
|
||||
};
|
||||
|
||||
get em() {
|
||||
@@ -60,23 +60,27 @@ describe("Module", async () => {
|
||||
Object.values(_em.entities),
|
||||
new DummyConnection(),
|
||||
_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) {
|
||||
return {
|
||||
entities: _em.entities.map((e) => ({
|
||||
name: e.name,
|
||||
fields: e.fields.map((f) => f.name),
|
||||
type: e.type
|
||||
type: e.type,
|
||||
})),
|
||||
indices: _em.indices.map((i) => ({
|
||||
name: i.name,
|
||||
entity: i.entity.name,
|
||||
fields: i.fields.map((f) => f.name),
|
||||
unique: i.unique
|
||||
}))
|
||||
unique: i.unique,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -88,15 +92,15 @@ describe("Module", async () => {
|
||||
|
||||
expect(flat(make(initial).em)).toEqual({
|
||||
entities: [],
|
||||
indices: []
|
||||
indices: [],
|
||||
});
|
||||
});
|
||||
|
||||
test("init", () => {
|
||||
const initial = em({
|
||||
users: entity("u", {
|
||||
name: text()
|
||||
})
|
||||
name: text(),
|
||||
}),
|
||||
});
|
||||
|
||||
const m = make(initial);
|
||||
@@ -107,18 +111,18 @@ describe("Module", async () => {
|
||||
{
|
||||
name: "u",
|
||||
fields: ["id", "name"],
|
||||
type: "regular"
|
||||
}
|
||||
type: "regular",
|
||||
},
|
||||
],
|
||||
indices: []
|
||||
indices: [],
|
||||
});
|
||||
});
|
||||
|
||||
test("ensure entity", () => {
|
||||
const initial = em({
|
||||
users: entity("u", {
|
||||
name: text()
|
||||
})
|
||||
name: text(),
|
||||
}),
|
||||
});
|
||||
|
||||
const m = make(initial);
|
||||
@@ -127,29 +131,24 @@ describe("Module", async () => {
|
||||
{
|
||||
name: "u",
|
||||
fields: ["id", "name"],
|
||||
type: "regular"
|
||||
}
|
||||
type: "regular",
|
||||
},
|
||||
],
|
||||
indices: []
|
||||
indices: [],
|
||||
});
|
||||
|
||||
// this should add a new entity
|
||||
m.prt.ensureEntity(
|
||||
entity("p", {
|
||||
title: text()
|
||||
})
|
||||
title: text(),
|
||||
}),
|
||||
);
|
||||
|
||||
// this should only add the field "important"
|
||||
m.prt.ensureEntity(
|
||||
entity(
|
||||
"u",
|
||||
{
|
||||
important: text()
|
||||
},
|
||||
undefined,
|
||||
"system"
|
||||
)
|
||||
entity("u", {
|
||||
important: text(),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(m.ctx.flags.sync_required).toBe(true);
|
||||
@@ -158,23 +157,22 @@ describe("Module", async () => {
|
||||
{
|
||||
name: "u",
|
||||
fields: ["id", "name", "important"],
|
||||
// ensured type must be present
|
||||
type: "system"
|
||||
type: "regular",
|
||||
},
|
||||
{
|
||||
name: "p",
|
||||
fields: ["id", "title"],
|
||||
type: "regular"
|
||||
}
|
||||
type: "regular",
|
||||
},
|
||||
],
|
||||
indices: []
|
||||
indices: [],
|
||||
});
|
||||
});
|
||||
|
||||
test("ensure index", () => {
|
||||
const users = entity("u", {
|
||||
name: text(),
|
||||
title: text()
|
||||
title: text(),
|
||||
});
|
||||
const initial = em({ users }, ({ index }, { users }) => {
|
||||
index(users).on(["title"]);
|
||||
@@ -189,23 +187,23 @@ describe("Module", async () => {
|
||||
{
|
||||
name: "u",
|
||||
fields: ["id", "name", "title"],
|
||||
type: "regular"
|
||||
}
|
||||
type: "regular",
|
||||
},
|
||||
],
|
||||
indices: [
|
||||
{
|
||||
name: "idx_u_title",
|
||||
entity: "u",
|
||||
fields: ["title"],
|
||||
unique: false
|
||||
unique: false,
|
||||
},
|
||||
{
|
||||
name: "idx_u_name",
|
||||
entity: "u",
|
||||
fields: ["name"],
|
||||
unique: false
|
||||
}
|
||||
]
|
||||
unique: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
||||
import { Type, disableConsoleLog, enableConsoleLog, stripMark } from "../../src/core/utils";
|
||||
import { entity, text } from "../../src/data";
|
||||
import { Module } from "../../src/modules/Module";
|
||||
import { ModuleManager, getDefaultConfig } from "../../src/modules/ModuleManager";
|
||||
import { CURRENT_VERSION, TABLE_NAME } from "../../src/modules/migrations";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||
|
||||
import { Module } from "modules/Module";
|
||||
import { type ConfigTable, getDefaultConfig, ModuleManager } from "modules/ModuleManager";
|
||||
import { CURRENT_VERSION, TABLE_NAME } from "modules/migrations";
|
||||
import { getDummyConnection } from "../helper";
|
||||
import { s, stripMark } from "core/utils/schema";
|
||||
import { Connection } from "data/connection/Connection";
|
||||
import { entity, text } from "data/prototype";
|
||||
|
||||
describe("ModuleManager", async () => {
|
||||
test("s1: no config, no build", async () => {
|
||||
@@ -39,11 +42,11 @@ describe("ModuleManager", async () => {
|
||||
basepath: "/api/data2",
|
||||
entities: {
|
||||
test: entity("test", {
|
||||
content: text()
|
||||
}).toJSON()
|
||||
}
|
||||
}
|
||||
});
|
||||
content: text(),
|
||||
}).toJSON(),
|
||||
},
|
||||
},
|
||||
}) as any;
|
||||
//const { version, ...json } = mm.toJSON() as any;
|
||||
|
||||
const c2 = getDummyConnection();
|
||||
@@ -69,11 +72,11 @@ describe("ModuleManager", async () => {
|
||||
basepath: "/api/data2",
|
||||
entities: {
|
||||
test: entity("test", {
|
||||
content: text()
|
||||
}).toJSON()
|
||||
}
|
||||
}
|
||||
};
|
||||
content: text(),
|
||||
}).toJSON(),
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
//const { version, ...json } = mm.toJSON() as any;
|
||||
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
@@ -89,24 +92,25 @@ describe("ModuleManager", async () => {
|
||||
|
||||
await mm2.build();
|
||||
|
||||
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();
|
||||
/* console.log({
|
||||
json,
|
||||
configs: mm2.configs(),
|
||||
}); */
|
||||
//expect(stripMark(json)).toEqual(stripMark(mm2.configs()));
|
||||
expect(mm2.configs().data.entities?.test).toBeDefined();
|
||||
expect(mm2.configs().data.entities?.test?.fields?.content).toBeDefined();
|
||||
expect(mm2.get("data").toJSON().entities?.test?.fields?.content).toBeDefined();
|
||||
});
|
||||
|
||||
test("s4: config given, table exists, version outdated, migrate", async () => {
|
||||
const c = getDummyConnection();
|
||||
const mm = new ModuleManager(c.dummyConnection);
|
||||
await mm.build();
|
||||
const version = mm.version();
|
||||
const json = mm.configs();
|
||||
|
||||
const c2 = getDummyConnection();
|
||||
const db = c2.dummyConnection.kysely;
|
||||
const mm2 = new ModuleManager(c2.dummyConnection, {
|
||||
initial: { version: version - 1, ...json }
|
||||
});
|
||||
const mm2 = new ModuleManager(c2.dummyConnection);
|
||||
await mm2.syncConfigTable();
|
||||
|
||||
await db
|
||||
@@ -129,7 +133,7 @@ describe("ModuleManager", async () => {
|
||||
const db = c2.dummyConnection.kysely;
|
||||
|
||||
const mm2 = new ModuleManager(c2.dummyConnection, {
|
||||
initial: { version: version - 1, ...json }
|
||||
initial: { version: version - 1, ...json },
|
||||
});
|
||||
await mm2.syncConfigTable();
|
||||
await db
|
||||
@@ -157,8 +161,8 @@ describe("ModuleManager", async () => {
|
||||
...json,
|
||||
data: {
|
||||
...json.data,
|
||||
basepath: "/api/data2"
|
||||
}
|
||||
basepath: "/api/data2",
|
||||
},
|
||||
};
|
||||
await db
|
||||
.insertInto(TABLE_NAME)
|
||||
@@ -171,7 +175,7 @@ describe("ModuleManager", async () => {
|
||||
expect(mm2.configs().data.basepath).toBe("/api/data2");
|
||||
});
|
||||
|
||||
test("blank app, modify config", async () => {
|
||||
/*test("blank app, modify config", async () => {
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
|
||||
const mm = new ModuleManager(dummyConnection);
|
||||
@@ -190,35 +194,37 @@ describe("ModuleManager", async () => {
|
||||
...configs.server,
|
||||
admin: {
|
||||
...configs.server.admin,
|
||||
color_scheme: "dark"
|
||||
}
|
||||
}
|
||||
color_scheme: "dark",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});*/
|
||||
|
||||
test("partial config given", async () => {
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
|
||||
const partial = {
|
||||
auth: {
|
||||
enabled: true
|
||||
}
|
||||
enabled: true,
|
||||
},
|
||||
};
|
||||
const mm = new ModuleManager(dummyConnection, {
|
||||
initial: partial
|
||||
initial: partial,
|
||||
});
|
||||
await mm.build();
|
||||
|
||||
expect(mm.version()).toBe(CURRENT_VERSION);
|
||||
expect(mm.built()).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 () => {
|
||||
const c = getDummyConnection();
|
||||
const mm = new ModuleManager(c.dummyConnection);
|
||||
await mm.build();
|
||||
console.log("==".repeat(30));
|
||||
console.log("");
|
||||
const json = mm.configs();
|
||||
|
||||
const c2 = getDummyConnection();
|
||||
@@ -227,9 +233,9 @@ describe("ModuleManager", async () => {
|
||||
const mm2 = new ModuleManager(c2.dummyConnection, {
|
||||
initial: {
|
||||
auth: {
|
||||
basepath: "/shouldnt/take/this"
|
||||
}
|
||||
}
|
||||
basepath: "/shouldnt/take/this",
|
||||
},
|
||||
},
|
||||
});
|
||||
await mm2.syncConfigTable();
|
||||
const payload = {
|
||||
@@ -237,15 +243,15 @@ describe("ModuleManager", async () => {
|
||||
auth: {
|
||||
...json.auth,
|
||||
enabled: true,
|
||||
basepath: "/api/auth2"
|
||||
}
|
||||
basepath: "/api/auth2",
|
||||
},
|
||||
};
|
||||
await db
|
||||
.insertInto(TABLE_NAME)
|
||||
.values({
|
||||
type: "config",
|
||||
json: JSON.stringify(payload),
|
||||
version: CURRENT_VERSION
|
||||
version: CURRENT_VERSION,
|
||||
})
|
||||
.execute();
|
||||
await mm2.build();
|
||||
@@ -255,18 +261,18 @@ describe("ModuleManager", async () => {
|
||||
// @todo: add tests for migrations (check "backup" and new version)
|
||||
|
||||
describe("revert", async () => {
|
||||
const failingModuleSchema = Type.Object({
|
||||
value: Type.Optional(Type.Number())
|
||||
const failingModuleSchema = s.partialObject({
|
||||
value: s.number(),
|
||||
});
|
||||
class FailingModule extends Module<typeof failingModuleSchema> {
|
||||
class FailingModule extends Module<s.Static<typeof failingModuleSchema>> {
|
||||
getSchema() {
|
||||
return failingModuleSchema;
|
||||
}
|
||||
|
||||
override async build() {
|
||||
//console.log("building FailingModule", this.config);
|
||||
if (this.config.value < 0) {
|
||||
throw new Error("value must be positive");
|
||||
if (this.config.value && this.config.value < 0) {
|
||||
throw new Error("value must be positive, given: " + this.config.value);
|
||||
}
|
||||
this.setBuilt();
|
||||
}
|
||||
@@ -301,8 +307,8 @@ describe("ModuleManager", async () => {
|
||||
const mm = new TestModuleManager(dummyConnection, {
|
||||
initial: {
|
||||
// @ts-ignore
|
||||
failing: { value: 2 }
|
||||
}
|
||||
failing: { value: 2 },
|
||||
},
|
||||
});
|
||||
await mm.build();
|
||||
expect(mm.configs()["failing"].value).toBe(2);
|
||||
@@ -313,8 +319,8 @@ describe("ModuleManager", async () => {
|
||||
const mm = new TestModuleManager(dummyConnection, {
|
||||
initial: {
|
||||
// @ts-ignore
|
||||
failing: { value: -1 }
|
||||
}
|
||||
failing: { value: -1 },
|
||||
},
|
||||
});
|
||||
expect(mm.build()).rejects.toThrow(/value must be positive/);
|
||||
expect(mm.configs()["failing"].value).toBe(-1);
|
||||
@@ -326,12 +332,13 @@ describe("ModuleManager", async () => {
|
||||
const mm = new TestModuleManager(dummyConnection, {
|
||||
onUpdated: async () => {
|
||||
mockOnUpdated();
|
||||
}
|
||||
},
|
||||
});
|
||||
await mm.build();
|
||||
// @ts-ignore
|
||||
const f = mm.mutateConfigSafe("failing");
|
||||
|
||||
// @ts-ignore
|
||||
expect(f.set({ value: 2 })).resolves.toBeDefined();
|
||||
expect(mockOnUpdated).toHaveBeenCalled();
|
||||
});
|
||||
@@ -342,11 +349,11 @@ describe("ModuleManager", async () => {
|
||||
const mm = new TestModuleManager(dummyConnection, {
|
||||
initial: {
|
||||
// @ts-ignore
|
||||
failing: { value: 1 }
|
||||
failing: { value: 1 },
|
||||
},
|
||||
onUpdated: async () => {
|
||||
mockOnUpdated();
|
||||
}
|
||||
},
|
||||
});
|
||||
await mm.build();
|
||||
expect(mm.configs()["failing"].value).toBe(1);
|
||||
@@ -354,7 +361,7 @@ describe("ModuleManager", async () => {
|
||||
// now safe mutate
|
||||
// @ts-ignore
|
||||
expect(mm.mutateConfigSafe("failing").set({ value: -2 })).rejects.toThrow(
|
||||
/value must be positive/
|
||||
/value must be positive/,
|
||||
);
|
||||
expect(mm.configs()["failing"].value).toBe(1);
|
||||
expect(mockOnUpdated).toHaveBeenCalled();
|
||||
@@ -380,4 +387,128 @@ describe("ModuleManager", async () => {
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { type InitialModuleConfigs, createApp } from "../../../src";
|
||||
|
||||
import { type Kysely, sql } from "kysely";
|
||||
import { getDummyConnection } from "../../helper";
|
||||
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
|
||||
async function createVersionedApp(config: InitialModuleConfigs | any) {
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
|
||||
if (!("version" in config)) throw new Error("config must have a version");
|
||||
const { version, ...rest } = config;
|
||||
|
||||
const db = dummyConnection.kysely as Kysely<any>;
|
||||
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);
|
||||
|
||||
await db
|
||||
.insertInto("__bknd")
|
||||
.values({
|
||||
version,
|
||||
type: "config",
|
||||
created_at: new Date().toISOString(),
|
||||
json: JSON.stringify(rest),
|
||||
})
|
||||
.execute();
|
||||
|
||||
const app = createApp({
|
||||
connection: dummyConnection,
|
||||
});
|
||||
await app.build();
|
||||
return app;
|
||||
}
|
||||
|
||||
describe("Migrations", () => {
|
||||
/**
|
||||
* updated auth strategies to have "enabled" prop
|
||||
* by default, migration should make all available strategies enabled
|
||||
*/
|
||||
test("migration from 7 to 8", async () => {
|
||||
expect(v7.version).toBe(7);
|
||||
|
||||
const app = await createVersionedApp(v7);
|
||||
|
||||
expect(app.version()).toBeGreaterThan(7);
|
||||
expect(app.toJSON(true).auth.strategies?.password?.enabled).toBe(true);
|
||||
|
||||
const req = await app.server.request("/api/auth/password/register", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: "test@test.com",
|
||||
password: "12345678",
|
||||
}),
|
||||
});
|
||||
expect(req.ok).toBe(true);
|
||||
const res = (await req.json()) as any;
|
||||
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,638 @@
|
||||
{
|
||||
"version": 7,
|
||||
"server": {
|
||||
"admin": {
|
||||
"basepath": "",
|
||||
"logo_return_path": "/",
|
||||
"color_scheme": "light"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": false,
|
||||
"fillable": true,
|
||||
"hidden": ["table"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"]
|
||||
},
|
||||
"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"]
|
||||
},
|
||||
"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
|
||||
}
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"sort_field": "id",
|
||||
"sort_dir": "asc"
|
||||
}
|
||||
},
|
||||
"product_likes": {
|
||||
"type": "regular",
|
||||
"fields": {
|
||||
"id": {
|
||||
"type": "primary",
|
||||
"config": {
|
||||
"fillable": false,
|
||||
"required": false,
|
||||
"hidden": false
|
||||
}
|
||||
},
|
||||
"created_at": {
|
||||
"type": "date",
|
||||
"config": {
|
||||
"type": "date",
|
||||
"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": {
|
||||
"name": "Product Likes",
|
||||
"sort_field": "id",
|
||||
"sort_dir": "asc"
|
||||
}
|
||||
},
|
||||
"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": true,
|
||||
"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
|
||||
}
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"sort_field": "id",
|
||||
"sort_dir": "asc"
|
||||
}
|
||||
},
|
||||
"boards_products": {
|
||||
"type": "generated",
|
||||
"fields": {
|
||||
"id": {
|
||||
"type": "primary",
|
||||
"config": {
|
||||
"fillable": false,
|
||||
"required": false,
|
||||
"hidden": false
|
||||
}
|
||||
},
|
||||
"boards_id": {
|
||||
"type": "relation",
|
||||
"config": {
|
||||
"required": false,
|
||||
"reference": "boards",
|
||||
"target": "boards",
|
||||
"target_field": "id",
|
||||
"fillable": true,
|
||||
"hidden": false,
|
||||
"on_delete": "set null"
|
||||
}
|
||||
},
|
||||
"products_id": {
|
||||
"type": "relation",
|
||||
"config": {
|
||||
"required": false,
|
||||
"reference": "products",
|
||||
"target": "products",
|
||||
"target_field": "id",
|
||||
"fillable": true,
|
||||
"hidden": false,
|
||||
"on_delete": "set null"
|
||||
}
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"sort_field": "id",
|
||||
"sort_dir": "asc"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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_media_entity_id": {
|
||||
"entity": "media",
|
||||
"fields": ["entity_id"],
|
||||
"unique": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"enabled": true,
|
||||
"basepath": "/api/auth",
|
||||
"entity_name": "users",
|
||||
"allow_register": true,
|
||||
"jwt": {
|
||||
"secret": "...",
|
||||
"alg": "HS256",
|
||||
"fields": ["id", "email", "role"]
|
||||
},
|
||||
"cookie": {
|
||||
"path": "/",
|
||||
"sameSite": "lax",
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"expires": 604800,
|
||||
"renew": true,
|
||||
"pathSuccess": "/",
|
||||
"pathLoggedOut": "/"
|
||||
},
|
||||
"strategies": {
|
||||
"password": {
|
||||
"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": "...",
|
||||
"secret_access_key": "...",
|
||||
"url": "https://some.r2.cloudflarestorage.com/some"
|
||||
}
|
||||
}
|
||||
},
|
||||
"flows": {
|
||||
"basepath": "/api/flows",
|
||||
"flows": {}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
import { beforeEach, describe, expect, it } from "bun:test";
|
||||
|
||||
import { Hono } from "hono";
|
||||
import { Guard } from "../../src/auth";
|
||||
import { DebugLogger } from "../../src/core";
|
||||
import { EventManager } from "../../src/core/events";
|
||||
import { Default, stripMark } from "../../src/core/utils";
|
||||
import { EntityManager } from "../../src/data";
|
||||
import { Module, type ModuleBuildContext } from "../../src/modules/Module";
|
||||
import { Guard } from "auth/authorize/Guard";
|
||||
import { DebugLogger } from "core/utils/DebugLogger";
|
||||
import { EventManager } from "core/events";
|
||||
import { EntityManager } from "data/entities/EntityManager";
|
||||
import { Module, type ModuleBuildContext } from "modules/Module";
|
||||
import { getDummyConnection } from "../helper";
|
||||
import { ModuleHelper } from "modules/ModuleHelper";
|
||||
|
||||
export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildContext {
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
return {
|
||||
const ctx = {
|
||||
connection: dummyConnection,
|
||||
server: new Hono(),
|
||||
em: new EntityManager([], dummyConnection),
|
||||
@@ -19,8 +19,12 @@ export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildCon
|
||||
guard: new Guard(),
|
||||
flags: Module.ctx_flags,
|
||||
logger: new DebugLogger(false),
|
||||
...overrides
|
||||
...overrides,
|
||||
};
|
||||
return {
|
||||
...ctx,
|
||||
helper: new ModuleHelper(ctx as any),
|
||||
} as any;
|
||||
}
|
||||
|
||||
export function moduleTestSuite(module: { new (): Module }) {
|
||||
@@ -40,7 +44,8 @@ export function moduleTestSuite(module: { new (): Module }) {
|
||||
it("uses the default config", async () => {
|
||||
const m = new module();
|
||||
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(), {}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ describe("json form", () => {
|
||||
["0", { type: "boolean" }, false],
|
||||
["on", { type: "boolean" }, true],
|
||||
["off", { type: "boolean" }, false],
|
||||
["null", { type: "null" }, null]
|
||||
["null", { type: "null" }, null],
|
||||
] satisfies [string, Exclude<JSONSchema, boolean>, any][];
|
||||
|
||||
for (const [input, schema, output] of examples) {
|
||||
@@ -35,7 +35,7 @@ describe("json form", () => {
|
||||
["array", "array", true],
|
||||
["object", "array", false],
|
||||
[["string", "number"], "number", true],
|
||||
["number", ["string", "number"], true]
|
||||
["number", ["string", "number"], true],
|
||||
] satisfies [IsTypeType, IsTypeType, boolean][];
|
||||
|
||||
for (const [type, schemaType, output] of examples) {
|
||||
@@ -48,7 +48,7 @@ describe("json form", () => {
|
||||
["#/nested/property/0/name", "#/nested/property/0"],
|
||||
["#/nested/property/0", "#/nested/property"],
|
||||
["#/nested/property", "#/nested"],
|
||||
["#/nested", "#"]
|
||||
["#/nested", "#"],
|
||||
];
|
||||
|
||||
for (const [input, output] of examples) {
|
||||
@@ -61,16 +61,16 @@ describe("json form", () => {
|
||||
[
|
||||
"#/description",
|
||||
{ type: "object", properties: { description: { type: "string" } } },
|
||||
false
|
||||
false,
|
||||
],
|
||||
[
|
||||
"#/description",
|
||||
{
|
||||
type: "object",
|
||||
required: ["description"],
|
||||
properties: { description: { type: "string" } }
|
||||
properties: { description: { type: "string" } },
|
||||
},
|
||||
true
|
||||
true,
|
||||
],
|
||||
[
|
||||
"#/nested/property",
|
||||
@@ -79,11 +79,11 @@ describe("json form", () => {
|
||||
properties: {
|
||||
nested: {
|
||||
type: "object",
|
||||
properties: { property: { type: "string" } }
|
||||
}
|
||||
}
|
||||
properties: { property: { type: "string" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
false
|
||||
false,
|
||||
],
|
||||
[
|
||||
"#/nested/property",
|
||||
@@ -93,12 +93,12 @@ describe("json form", () => {
|
||||
nested: {
|
||||
type: "object",
|
||||
required: ["property"],
|
||||
properties: { property: { type: "string" } }
|
||||
}
|
||||
}
|
||||
properties: { property: { type: "string" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
true
|
||||
]
|
||||
true,
|
||||
],
|
||||
] satisfies [string, Exclude<JSONSchema, boolean>, boolean][];
|
||||
|
||||
for (const [pointer, schema, output] of examples) {
|
||||
@@ -113,7 +113,7 @@ describe("json form", () => {
|
||||
["tags", "0", "0.tags"],
|
||||
["tags", 0, "0.tags"],
|
||||
["nested.property", "prefix", "prefix.nested.property"],
|
||||
["nested.property", "", "nested.property"]
|
||||
["nested.property", "", "nested.property"],
|
||||
] satisfies [string, any, string][];
|
||||
|
||||
for (const [path, prefix, output] of examples) {
|
||||
@@ -128,7 +128,7 @@ describe("json form", () => {
|
||||
["tags", "0", "tags.0"],
|
||||
["tags", 0, "tags.0"],
|
||||
["nested.property", "suffix", "nested.property.suffix"],
|
||||
["nested.property", "", "nested.property"]
|
||||
["nested.property", "", "nested.property"],
|
||||
] satisfies [string, any, string][];
|
||||
|
||||
for (const [path, suffix, output] of examples) {
|
||||
|
||||
@@ -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()]);
|
||||
}
|
||||
+112
-40
@@ -1,5 +1,6 @@
|
||||
import { $ } from "bun";
|
||||
import * as tsup from "tsup";
|
||||
import pkg from "./package.json" with { type: "json" };
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const watch = args.includes("--watch");
|
||||
@@ -8,8 +9,13 @@ const types = args.includes("--types");
|
||||
const sourcemap = args.includes("--sourcemap");
|
||||
const clean = args.includes("--clean");
|
||||
|
||||
const define = {
|
||||
__isDev: "0",
|
||||
__version: JSON.stringify(pkg.version),
|
||||
};
|
||||
|
||||
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 {} +`;
|
||||
}
|
||||
|
||||
@@ -21,15 +27,15 @@ function buildTypes() {
|
||||
Bun.spawn(["bun", "build:types"], {
|
||||
stdout: "inherit",
|
||||
onExit: () => {
|
||||
console.log("Types built");
|
||||
console.info("Types built");
|
||||
Bun.spawn(["bun", "tsc-alias"], {
|
||||
stdout: "inherit",
|
||||
onExit: () => {
|
||||
console.log("Types aliased");
|
||||
console.info("Types aliased");
|
||||
types_running = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -46,43 +52,66 @@ if (types && !watch) {
|
||||
buildTypes();
|
||||
}
|
||||
|
||||
function banner(title: string) {
|
||||
console.info("");
|
||||
console.info("=".repeat(40));
|
||||
console.info(title.toUpperCase());
|
||||
console.info("-".repeat(40));
|
||||
}
|
||||
|
||||
// collection of always-external packages
|
||||
const external = [
|
||||
"bun:test",
|
||||
"node:test",
|
||||
"node:assert/strict",
|
||||
"@libsql/client",
|
||||
"bknd",
|
||||
/^bknd\/.*/,
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Building backend and general API
|
||||
*/
|
||||
async function buildApi() {
|
||||
banner("Building API");
|
||||
await tsup.build({
|
||||
minify,
|
||||
sourcemap,
|
||||
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/utils/index.ts", "src/plugins/index.ts"],
|
||||
outDir: "dist",
|
||||
external: ["bun:test", "@libsql/client", "bknd/client"],
|
||||
external: [...external],
|
||||
metafile: true,
|
||||
platform: "browser",
|
||||
format: ["esm"],
|
||||
splitting: false,
|
||||
treeshake: true,
|
||||
loader: {
|
||||
".svg": "dataurl"
|
||||
".svg": "dataurl",
|
||||
},
|
||||
onSuccess: async () => {
|
||||
delayTypes();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function rewriteClient(path: string) {
|
||||
const bundle = await Bun.file(path).text();
|
||||
await Bun.write(path, '"use client";\n' + bundle.replaceAll("ui/client", "bknd/client"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Building UI for direct imports
|
||||
*/
|
||||
async function buildUi() {
|
||||
await tsup.build({
|
||||
const base = {
|
||||
minify,
|
||||
sourcemap,
|
||||
watch,
|
||||
entry: ["src/ui/index.ts", "src/ui/client/index.ts", "src/ui/main.css", "src/ui/styles.css"],
|
||||
outDir: "dist/ui",
|
||||
define,
|
||||
external: [
|
||||
"bun:test",
|
||||
...external,
|
||||
"react",
|
||||
"react-dom",
|
||||
"react/jsx-runtime",
|
||||
@@ -90,7 +119,7 @@ async function buildUi() {
|
||||
"use-sync-external-store",
|
||||
/codemirror/,
|
||||
"@xyflow/react",
|
||||
"@mantine/core"
|
||||
"@mantine/core",
|
||||
],
|
||||
metafile: true,
|
||||
platform: "browser",
|
||||
@@ -99,14 +128,33 @@ async function buildUi() {
|
||||
bundle: true,
|
||||
treeshake: true,
|
||||
loader: {
|
||||
".svg": "dataurl"
|
||||
".svg": "dataurl",
|
||||
},
|
||||
esbuildOptions: (options) => {
|
||||
options.logLevel = "silent";
|
||||
},
|
||||
} satisfies tsup.Options;
|
||||
|
||||
banner("Building UI");
|
||||
await tsup.build({
|
||||
...base,
|
||||
entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"],
|
||||
outDir: "dist/ui",
|
||||
onSuccess: async () => {
|
||||
await rewriteClient("./dist/ui/index.js");
|
||||
delayTypes();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
banner("Building Client");
|
||||
await tsup.build({
|
||||
...base,
|
||||
entry: ["src/ui/client/index.ts"],
|
||||
outDir: "dist/ui/client",
|
||||
onSuccess: async () => {
|
||||
await rewriteClient("./dist/ui/client/index.js");
|
||||
delayTypes();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -116,10 +164,12 @@ async function buildUi() {
|
||||
* - ui/client is external, and after built replaced with "bknd/client"
|
||||
*/
|
||||
async function buildUiElements() {
|
||||
banner("Building UI Elements");
|
||||
await tsup.build({
|
||||
minify,
|
||||
sourcemap,
|
||||
watch,
|
||||
define,
|
||||
entry: ["src/ui/elements/index.ts"],
|
||||
outDir: "dist/ui/elements",
|
||||
external: [
|
||||
@@ -128,7 +178,7 @@ async function buildUiElements() {
|
||||
"react-dom",
|
||||
"react/jsx-runtime",
|
||||
"react/jsx-dev-runtime",
|
||||
"use-sync-external-store"
|
||||
"use-sync-external-store",
|
||||
],
|
||||
metafile: true,
|
||||
platform: "browser",
|
||||
@@ -137,22 +187,18 @@ async function buildUiElements() {
|
||||
bundle: true,
|
||||
treeshake: true,
|
||||
loader: {
|
||||
".svg": "dataurl"
|
||||
".svg": "dataurl",
|
||||
},
|
||||
esbuildOptions: (options) => {
|
||||
options.alias = {
|
||||
// not important for elements, mock to reduce bundle
|
||||
"tailwind-merge": "./src/ui/elements/mocks/tailwind-merge.ts"
|
||||
"tailwind-merge": "./src/ui/elements/mocks/tailwind-merge.ts",
|
||||
};
|
||||
},
|
||||
onSuccess: async () => {
|
||||
// manually replace ui/client with bknd/client
|
||||
const path = "./dist/ui/elements/index.js";
|
||||
const bundle = await Bun.file(path).text();
|
||||
await Bun.write(path, bundle.replaceAll("ui/client", "bknd/client"));
|
||||
|
||||
await rewriteClient("./dist/ui/elements/index.js");
|
||||
delayTypes();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -175,50 +221,76 @@ function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsu
|
||||
},
|
||||
...overrides,
|
||||
define: {
|
||||
__isDev: "0",
|
||||
...overrides.define
|
||||
...define,
|
||||
...overrides.define,
|
||||
},
|
||||
external: [
|
||||
/^cloudflare*/,
|
||||
/^@?(hono|libsql).*?/,
|
||||
/^@?hono.*?/,
|
||||
/^(bknd|react|next|node).*?/,
|
||||
/.*\.(html)$/,
|
||||
...(Array.isArray(overrides.external) ? overrides.external : [])
|
||||
]
|
||||
...external,
|
||||
...(Array.isArray(overrides.external) ? overrides.external : []),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async function buildAdapters() {
|
||||
banner("Building Adapters");
|
||||
// base adapter handles
|
||||
await tsup.build({
|
||||
...baseConfig(""),
|
||||
entry: ["src/adapter/index.ts"],
|
||||
outDir: "dist/adapter"
|
||||
outDir: "dist/adapter",
|
||||
});
|
||||
|
||||
// specific adatpers
|
||||
await tsup.build(baseConfig("remix"));
|
||||
await tsup.build(baseConfig("bun"));
|
||||
await tsup.build(baseConfig("astro"));
|
||||
await tsup.build(baseConfig("react-router"));
|
||||
await tsup.build(
|
||||
baseConfig("cloudflare", {
|
||||
external: [/^kysely/]
|
||||
})
|
||||
baseConfig("bun", {
|
||||
external: [/^bun\:.*/],
|
||||
}),
|
||||
);
|
||||
await tsup.build(baseConfig("astro"));
|
||||
await tsup.build(baseConfig("aws"));
|
||||
await tsup.build(baseConfig("cloudflare"));
|
||||
|
||||
await tsup.build({
|
||||
...baseConfig("vite"),
|
||||
platform: "node"
|
||||
platform: "node",
|
||||
});
|
||||
|
||||
await tsup.build({
|
||||
...baseConfig("nextjs"),
|
||||
platform: "node"
|
||||
platform: "node",
|
||||
});
|
||||
|
||||
await tsup.build({
|
||||
...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\:.*/],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user