mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 16:16:02 +00:00
Compare commits
210 Commits
v0.7.1
...
feat/init-e2e
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ff310d6c4 | |||
| 5178dbee0d | |||
| f0f2b571b5 | |||
| 2cff116fcf | |||
| 0b41aa5a2d | |||
| a5ec40c517 | |||
| 53a48b4b6b | |||
| 6f92ef7b74 | |||
| e3628a3dc8 | |||
| fd4bbccfb7 | |||
| 75e2b96344 | |||
| aaae8d9681 | |||
| e4608b7df7 | |||
| 44b3f72005 | |||
| 9134d121cd | |||
| 2f067451b4 | |||
| 3f26c45dd9 | |||
| 36e4224b33 | |||
| 434d56672c | |||
| b2fd907e8c | |||
| b29c04e8c9 | |||
| 11a28eba88 | |||
| 7ed5db5eaa | |||
| 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 | |||
| 9ecfcb3e05 | |||
| fb3dbd343b | |||
| 060df210f9 | |||
| fb725be0ed | |||
| 1a95b4c8bb | |||
| cfa950aa46 | |||
| 5771298bfa | |||
| 94c40de011 | |||
| 6f776aeff7 | |||
| eaa7276173 | |||
| 89871a8ef7 | |||
| 7f0f6bec08 | |||
| 8e1ad98964 | |||
| a29e824ecc | |||
| b436758fe6 | |||
| ee7dcd44f4 | |||
| f2e5815e24 | |||
| 335fe42a22 | |||
| a01b99be27 | |||
| a4e46a2768 | |||
| f494735a79 | |||
| 400db84dd5 | |||
| 964bacd824 | |||
| 7994eabe7d | |||
| dec382b49d | |||
| 93cee4f8aa | |||
| e99abb09b6 | |||
| a5ab102b34 | |||
| 1ca6c5b43d | |||
| 64658184ac | |||
| 61ad4a3109 | |||
| e12ed1e4b3 | |||
| e103d3ac33 | |||
| e2217c411a | |||
| 3e6d381239 | |||
| 0823b182f6 | |||
| bd362607ae | |||
| 61ac31eba9 | |||
| 21a8f56c48 | |||
| fb76f8d789 | |||
| 4bd201d454 | |||
| b412da836c | |||
| 425465becd | |||
| 3b487ade2a | |||
| 7d3c76d7c1 | |||
| fd297c6c50 | |||
| 149a6d768c | |||
| 24e69eec90 | |||
| 9c38c8f2ad | |||
| f0bdb3a75e | |||
| c4e505582b | |||
| cc938db4b8 | |||
| 68277a68d5 | |||
| 8e72b5b615 | |||
| 07422f30ba | |||
| d84731d89e | |||
| 4afea14893 | |||
| 2c6a4d2bed | |||
| f466dd166d | |||
| 6d8f86b2c9 | |||
| c8fa704e32 | |||
| 61d06973a4 | |||
| 7a6321f4ce | |||
| 4dde67ca21 | |||
| f9c612e0fa | |||
| d7e772efd9 | |||
| be39d1c374 |
@@ -0,0 +1,34 @@
|
|||||||
|
name: Run Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v1
|
||||||
|
with:
|
||||||
|
bun-version: "1.2.5"
|
||||||
|
|
||||||
|
- 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
|
||||||
@@ -19,6 +19,7 @@ packages/media/.env
|
|||||||
.history
|
.history
|
||||||
**/*/.db/*
|
**/*/.db/*
|
||||||
**/*/.configs/*
|
**/*/.configs/*
|
||||||
|
**/*/.template/*
|
||||||
**/*/*.db
|
**/*/*.db
|
||||||
**/*/*.db-shm
|
**/*/*.db-shm
|
||||||
**/*/*.db-wal
|
**/*/*.db-wal
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||

|

|
||||||
|
|
||||||
<p align="center" width="100%">
|
<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>
|
<strong>⭐ Live Demo</strong>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
@@ -18,13 +18,15 @@ bknd simplifies app development by providing a fully functional backend for data
|
|||||||
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
|
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
|
||||||
|
|
||||||
## Size
|
## Size
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
The size on npm is misleading, as the `bknd` package includes the backend, the ui components as well as the whole backend bundled into the cli including static assets.
|
The size on npm is misleading, as the `bknd` package includes the backend, the ui components as well as the whole backend bundled into the cli including static assets.
|
||||||
|
|
||||||
|
Depending on what you use, the size can be higher as additional dependencies are getting pulled in. The minimal size of a full `bknd` app as an API is around 212 kB gzipped (e.g. deployed as Cloudflare Worker).
|
||||||
|
|
||||||
## Motivation
|
## Motivation
|
||||||
Creating digital products always requires developing both the backend (the logic) and the frontend (the appearance). Building a backend from scratch demands deep knowledge in areas such as authentication and database management. Using a backend framework can speed up initial development, but it still requires ongoing effort to work within its constraints (e.g., *"how to do X with Y?"*), which can quickly slow you down. Choosing a backend system is a tough decision, as you might not be aware of its limitations until you encounter them.
|
Creating digital products always requires developing both the backend (the logic) and the frontend (the appearance). Building a backend from scratch demands deep knowledge in areas such as authentication and database management. Using a backend framework can speed up initial development, but it still requires ongoing effort to work within its constraints (e.g., *"how to do X with Y?"*), which can quickly slow you down. Choosing a backend system is a tough decision, as you might not be aware of its limitations until you encounter them.
|
||||||
|
|
||||||
@@ -40,7 +42,7 @@ Creating digital products always requires developing both the backend (the logic
|
|||||||
* 🏃♂️ Multiple run modes
|
* 🏃♂️ Multiple run modes
|
||||||
* standalone using the CLI
|
* standalone using the CLI
|
||||||
* using a JavaScript runtime (Node, Bun, workerd)
|
* using a JavaScript runtime (Node, Bun, workerd)
|
||||||
* using a React framework (Astro, Remix, Next.js)
|
* using a React framework (Next.js, React Router, Astro)
|
||||||
* 📦 Official API and React SDK with type-safety
|
* 📦 Official API and React SDK with type-safety
|
||||||
* ⚛️ React elements for auto-configured authentication and media components
|
* ⚛️ React elements for auto-configured authentication and media components
|
||||||
|
|
||||||
@@ -78,7 +80,7 @@ export default function AdminPage() {
|
|||||||
### Using the REST API or TypeScript SDK (`bknd/client`)
|
### 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:
|
If you're not using a JavaScript environment, you can still access any endpoint using the REST API:
|
||||||
```bash
|
```bash
|
||||||
curl -XGET <your-endpoint>/api/data/<entity>
|
curl -XGET <your-endpoint>/api/data/entity/<entity>
|
||||||
{
|
{
|
||||||
"data": [
|
"data": [
|
||||||
{ "id": 1, ... },
|
{ "id": 1, ... },
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
playwright-report
|
||||||
|
test-results
|
||||||
|
bknd.config.*
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, afterEach, describe, expect, test } from "bun:test";
|
||||||
import { App } from "../src";
|
import { App } from "../src";
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
afterAll(afterAllCleanup);
|
afterEach(afterAllCleanup);
|
||||||
|
|
||||||
describe("App tests", async () => {
|
describe("App tests", async () => {
|
||||||
test("boots and pongs", async () => {
|
test("boots and pongs", async () => {
|
||||||
@@ -12,4 +12,16 @@ describe("App tests", async () => {
|
|||||||
|
|
||||||
//expect(await app.data?.em.ping()).toBeTrue();
|
//expect(await app.data?.em.ping()).toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*test.only("what", async () => {
|
||||||
|
const app = new App(dummyConnection, {
|
||||||
|
auth: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await app.module.auth.build();
|
||||||
|
await app.module.data.build();
|
||||||
|
console.log(app.em.entities.map((e) => e.name));
|
||||||
|
console.log(await app.em.schema().getDiff());
|
||||||
|
});*/
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
tmp/*
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
@@ -0,0 +1,62 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
beforeAll(disableConsoleLog);
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
|
describe("adapter", () => {
|
||||||
|
it("makes config", () => {
|
||||||
|
expect(adapter.makeConfig({})).toEqual({});
|
||||||
|
expect(adapter.makeConfig({}, { env: { TEST: "test" } })).toEqual({});
|
||||||
|
|
||||||
|
// merges everything returned from `app` with the config
|
||||||
|
expect(adapter.makeConfig({ app: (a) => a as any }, { env: { TEST: "test" } })).toEqual({
|
||||||
|
env: { TEST: "test" },
|
||||||
|
} as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
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 token = await sign({ foo: "bar" }, "1234");
|
||||||
const request = new Request("http://example.com/test", {
|
const request = new Request("http://example.com/test", {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`
|
Authorization: `Bearer ${token}`,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
const api = new Api({ request });
|
const api = new Api({ request });
|
||||||
expect(api.isAuthVerified()).toBe(false);
|
expect(api.isAuthVerified()).toBe(false);
|
||||||
@@ -35,8 +35,8 @@ describe("Api", async () => {
|
|||||||
const token = await sign({ foo: "bar" }, "1234");
|
const token = await sign({ foo: "bar" }, "1234");
|
||||||
const request = new Request("http://example.com/test", {
|
const request = new Request("http://example.com/test", {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `auth=${token}`
|
Cookie: `auth=${token}`,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
const api = new Api({ request });
|
const api = new Api({ request });
|
||||||
expect(api.isAuthVerified()).toBe(false);
|
expect(api.isAuthVerified()).toBe(false);
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { DataApi } from "../../src/data/api/DataApi";
|
|||||||
import { DataController } from "../../src/data/api/DataController";
|
import { DataController } from "../../src/data/api/DataController";
|
||||||
import { dataConfigSchema } from "../../src/data/data-schema";
|
import { dataConfigSchema } from "../../src/data/data-schema";
|
||||||
import * as proto from "../../src/data/prototype";
|
import * as proto from "../../src/data/prototype";
|
||||||
import { disableConsoleLog, enableConsoleLog, schemaToEm } from "../helper";
|
import { schemaToEm } from "../helper";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
beforeAll(disableConsoleLog);
|
||||||
afterAll(enableConsoleLog);
|
afterAll(enableConsoleLog);
|
||||||
@@ -17,16 +18,16 @@ describe("DataApi", () => {
|
|||||||
|
|
||||||
const get = api.readMany("a".repeat(300), { select: ["id", "name"] });
|
const get = api.readMany("a".repeat(300), { select: ["id", "name"] });
|
||||||
expect(get.request.method).toBe("GET");
|
expect(get.request.method).toBe("GET");
|
||||||
expect(new URL(get.request.url).pathname).toBe(`/api/data/${"a".repeat(300)}`);
|
expect(new URL(get.request.url).pathname).toBe(`/api/data/entity/${"a".repeat(300)}`);
|
||||||
|
|
||||||
const post = api.readMany("a".repeat(1000), { select: ["id", "name"] });
|
const post = api.readMany("a".repeat(1000), { select: ["id", "name"] });
|
||||||
expect(post.request.method).toBe("POST");
|
expect(post.request.method).toBe("POST");
|
||||||
expect(new URL(post.request.url).pathname).toBe(`/api/data/${"a".repeat(1000)}/query`);
|
expect(new URL(post.request.url).pathname).toBe(`/api/data/entity/${"a".repeat(1000)}/query`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns result", async () => {
|
it("returns result", async () => {
|
||||||
const schema = proto.em({
|
const schema = proto.em({
|
||||||
posts: proto.entity("posts", { title: proto.text() })
|
posts: proto.entity("posts", { title: proto.text() }),
|
||||||
});
|
});
|
||||||
const em = schemaToEm(schema);
|
const em = schemaToEm(schema);
|
||||||
await em.schema().sync({ force: true });
|
await em.schema().sync({ force: true });
|
||||||
@@ -39,20 +40,18 @@ describe("DataApi", () => {
|
|||||||
const app = controller.getController();
|
const app = controller.getController();
|
||||||
|
|
||||||
{
|
{
|
||||||
const res = (await app.request("/posts")) as Response;
|
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);
|
expect(data.length).toEqual(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore tests
|
// @ts-ignore tests
|
||||||
const api = new DataApi({ basepath: "/", queryLengthLimit: 50 });
|
const api = new DataApi({ basepath: "/", queryLengthLimit: 50 }, app.request as typeof fetch);
|
||||||
// @ts-ignore protected
|
|
||||||
api.fetcher = app.request as typeof fetch;
|
|
||||||
{
|
{
|
||||||
const req = api.readMany("posts", { select: ["title"] });
|
const req = api.readMany("posts", { select: ["title"] });
|
||||||
expect(req.request.method).toBe("GET");
|
expect(req.request.method).toBe("GET");
|
||||||
const res = await req;
|
const res = await req;
|
||||||
expect(res.data).toEqual(payload);
|
expect(res.data).toEqual(payload as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -60,11 +59,164 @@ describe("DataApi", () => {
|
|||||||
select: ["title"],
|
select: ["title"],
|
||||||
limit: 100000,
|
limit: 100000,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
sort: "id"
|
sort: "id",
|
||||||
});
|
});
|
||||||
expect(req.request.method).toBe("POST");
|
expect(req.request.method).toBe("POST");
|
||||||
const res = await req;
|
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.count).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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
/// <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 { assetsPath, assetsTmpPath } from "../helper";
|
||||||
|
|
||||||
|
const mockedBackend = new Hono()
|
||||||
|
.basePath("/api/media")
|
||||||
|
.post("/upload/:name", async (c) => {
|
||||||
|
const { name } = c.req.param();
|
||||||
|
const body = await getFileFromContext(c);
|
||||||
|
return c.json({ name, is_file: isFile(body), size: body.size });
|
||||||
|
})
|
||||||
|
.get("/file/:name", async (c) => {
|
||||||
|
const { name } = c.req.param();
|
||||||
|
const file = Bun.file(`${assetsPath}/${name}`);
|
||||||
|
return new Response(file, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": file.type,
|
||||||
|
"Content-Length": file.size.toString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("MediaApi", () => {
|
||||||
|
it("should give correct file upload url", () => {
|
||||||
|
const host = "http://localhost";
|
||||||
|
const basepath = "/api/media";
|
||||||
|
// @ts-ignore tests
|
||||||
|
const api = new MediaApi({
|
||||||
|
host,
|
||||||
|
basepath,
|
||||||
|
});
|
||||||
|
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",
|
||||||
|
});
|
||||||
|
expect(api.getUploadHeaders().get("Authorization")).toBe("Bearer token");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get file: native", async () => {
|
||||||
|
const name = "image.png";
|
||||||
|
const path = `${assetsTmpPath}/${name}`;
|
||||||
|
const res = await mockedBackend.request("/api/media/file/" + name);
|
||||||
|
await Bun.write(path, res);
|
||||||
|
|
||||||
|
const file = await Bun.file(path);
|
||||||
|
expect(file.size).toBeGreaterThan(0);
|
||||||
|
expect(file.type).toBe("image/png");
|
||||||
|
await file.delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("download", async () => {
|
||||||
|
// @ts-ignore tests
|
||||||
|
const api = new MediaApi({}, mockedBackend.request);
|
||||||
|
|
||||||
|
const name = "image.png";
|
||||||
|
const file = await api.download(name);
|
||||||
|
expect(isFile(file)).toBe(true);
|
||||||
|
expect(file.size).toBeGreaterThan(0);
|
||||||
|
expect(file.type).toBe("image/png");
|
||||||
|
expect(file.name).toContain(name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getFile", async () => {
|
||||||
|
// @ts-ignore tests
|
||||||
|
const api = new MediaApi({}, mockedBackend.request);
|
||||||
|
|
||||||
|
const name = "image.png";
|
||||||
|
const res = await api.getFile(name);
|
||||||
|
expect(res.ok).toBe(true);
|
||||||
|
// make sure it's a normal api request as usual
|
||||||
|
expect(res.res.ok).toBe(true);
|
||||||
|
expect(isReadableStream(res)).toBe(true);
|
||||||
|
expect(isReadableStream(res.body)).toBe(true);
|
||||||
|
expect(isReadableStream(res.res.body)).toBe(true);
|
||||||
|
|
||||||
|
const blob = await res.res.blob();
|
||||||
|
expect(isFile(blob)).toBe(true);
|
||||||
|
expect(blob.size).toBeGreaterThan(0);
|
||||||
|
expect(blob.type).toBe("image/png");
|
||||||
|
expect(blob.name).toContain(name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getFileStream", async () => {
|
||||||
|
// @ts-ignore tests
|
||||||
|
const api = new MediaApi({}, mockedBackend.request);
|
||||||
|
|
||||||
|
const name = "image.png";
|
||||||
|
const res = await api.getFileStream(name);
|
||||||
|
expect(isReadableStream(res)).toBe(true);
|
||||||
|
|
||||||
|
const blob = await new Response(res).blob();
|
||||||
|
expect(isFile(blob)).toBe(true);
|
||||||
|
expect(blob.size).toBeGreaterThan(0);
|
||||||
|
expect(blob.type).toBe("image/png");
|
||||||
|
expect(blob.name).toContain(name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should upload file in various ways", async () => {
|
||||||
|
// @ts-ignore tests
|
||||||
|
const api = new MediaApi({}, mockedBackend.request);
|
||||||
|
const file = Bun.file(`${assetsPath}/image.png`);
|
||||||
|
|
||||||
|
async function matches(req: Promise<any>, filename: string) {
|
||||||
|
const res: any = await req;
|
||||||
|
expect(res.name).toBe(filename);
|
||||||
|
expect(res.is_file).toBe(true);
|
||||||
|
expect(res.size).toBe(file.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = "http://localhost/api/media/file/image.png";
|
||||||
|
|
||||||
|
// upload bun file
|
||||||
|
await matches(api.upload(file as any, { filename: "bunfile.png" }), "bunfile.png");
|
||||||
|
|
||||||
|
// upload via request
|
||||||
|
await matches(api.upload(new Request(url), { filename: "request.png" }), "request.png");
|
||||||
|
|
||||||
|
// upload via url
|
||||||
|
await matches(api.upload(url, { filename: "url.png" }), "url.png");
|
||||||
|
|
||||||
|
// upload via response
|
||||||
|
{
|
||||||
|
const response = await mockedBackend.request(url);
|
||||||
|
await matches(api.upload(response, { filename: "response.png" }), "response.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
// upload via readable from bun
|
||||||
|
await matches(await api.upload(file.stream(), { filename: "readable.png" }), "readable.png");
|
||||||
|
|
||||||
|
// upload via readable from response
|
||||||
|
{
|
||||||
|
const response = (await mockedBackend.request(url)) as Response;
|
||||||
|
await matches(
|
||||||
|
await api.upload(response.body!, { filename: "readable.png" }),
|
||||||
|
"readable.png",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -61,7 +61,7 @@ describe("ModuleApi", () => {
|
|||||||
|
|
||||||
it("adds additional headers from options", () => {
|
it("adds additional headers from options", () => {
|
||||||
const headers = new Headers({
|
const headers = new Headers({
|
||||||
"X-Test": "123"
|
"X-Test": "123",
|
||||||
});
|
});
|
||||||
const api = new Api({ host, headers });
|
const api = new Api({ host, headers });
|
||||||
expect(api.get("/").request.headers.get("X-Test")).toEqual("123");
|
expect(api.get("/").request.headers.get("X-Test")).toEqual("123");
|
||||||
@@ -75,7 +75,7 @@ describe("ModuleApi", () => {
|
|||||||
it("uses search params", () => {
|
it("uses search params", () => {
|
||||||
const api = new Api({ host });
|
const api = new Api({ host });
|
||||||
const search = new URLSearchParams({
|
const search = new URLSearchParams({
|
||||||
foo: "bar"
|
foo: "bar",
|
||||||
});
|
});
|
||||||
expect(api.get("/", search).request.url).toEqual("http://localhost/?" + search.toString());
|
expect(api.get("/", search).request.url).toEqual("http://localhost/?" + search.toString());
|
||||||
});
|
});
|
||||||
@@ -89,6 +89,14 @@ describe("ModuleApi", () => {
|
|||||||
expect(api.delete("/").request.method).toEqual("DELETE");
|
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 error response
|
||||||
// @todo: test method shortcut functions
|
// @todo: test method shortcut functions
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import { describe, expect, mock, test } from "bun:test";
|
||||||
|
import type { ModuleBuildContext } from "../../src";
|
||||||
|
import { App, createApp } from "../../src/App";
|
||||||
|
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",
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { createApp, registries } from "../../src";
|
import { createApp, registries } from "../../src";
|
||||||
import * as proto from "../../src/data/prototype";
|
import * as proto from "../../src/data/prototype";
|
||||||
import { StorageLocalAdapter } from "../../src/media/storage/adapters/StorageLocalAdapter";
|
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||||
|
|
||||||
describe("repros", async () => {
|
describe("repros", async () => {
|
||||||
/**
|
/**
|
||||||
@@ -24,9 +24,9 @@ describe("repros", async () => {
|
|||||||
adapter: {
|
adapter: {
|
||||||
type: "local",
|
type: "local",
|
||||||
config: {
|
config: {
|
||||||
path: "./"
|
path: "./",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(config.enabled).toBe(true);
|
expect(config.enabled).toBe(true);
|
||||||
@@ -38,9 +38,9 @@ describe("repros", async () => {
|
|||||||
"entities.test",
|
"entities.test",
|
||||||
proto
|
proto
|
||||||
.entity("test", {
|
.entity("test", {
|
||||||
content: proto.text()
|
content: proto.text(),
|
||||||
})
|
})
|
||||||
.toJSON()
|
.toJSON(),
|
||||||
);
|
);
|
||||||
expect(app.em.entities.map((e) => e.name)).toContain("test");
|
expect(app.em.entities.map((e) => e.name)).toContain("test");
|
||||||
}
|
}
|
||||||
@@ -54,8 +54,8 @@ describe("repros", async () => {
|
|||||||
hidden: false,
|
hidden: false,
|
||||||
mime_types: [],
|
mime_types: [],
|
||||||
virtual: true,
|
virtual: true,
|
||||||
entity: "test"
|
entity: "test",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
@@ -63,11 +63,41 @@ describe("repros", async () => {
|
|||||||
type: "poly",
|
type: "poly",
|
||||||
source: "test",
|
source: "test",
|
||||||
target: "media",
|
target: "media",
|
||||||
config: { mappedBy: "files" }
|
config: { mappedBy: "files" },
|
||||||
})
|
}),
|
||||||
).resolves.toBeDefined();
|
).resolves.toBeDefined();
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(app.em.entities.map((e) => e.name)).toEqual(["media", "test"]);
|
expect(app.em.entities.map((e) => e.name)).toEqual(["media", "test"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.only("verify inversedBy", async () => {
|
||||||
|
const schema = proto.em(
|
||||||
|
{
|
||||||
|
products: proto.entity("products", {
|
||||||
|
title: proto.text(),
|
||||||
|
}),
|
||||||
|
product_likes: proto.entity("product_likes", {
|
||||||
|
created_at: proto.date(),
|
||||||
|
}),
|
||||||
|
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();
|
||||||
|
|
||||||
|
const info = (await (await app.server.request("/api/data/info/products")).json()) as any;
|
||||||
|
|
||||||
|
expect(info.fields).toEqual(["id", "title"]);
|
||||||
|
expect(info.relations.listable).toEqual([
|
||||||
|
{
|
||||||
|
entity: "product_likes",
|
||||||
|
ref: "likes",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,19 +7,17 @@ describe("authorize", () => {
|
|||||||
["read", "write"],
|
["read", "write"],
|
||||||
{
|
{
|
||||||
admin: {
|
admin: {
|
||||||
permissions: ["read", "write"]
|
permissions: ["read", "write"],
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{ enabled: true }
|
},
|
||||||
|
{ enabled: true },
|
||||||
);
|
);
|
||||||
const user = {
|
const user = {
|
||||||
role: "admin"
|
role: "admin",
|
||||||
};
|
};
|
||||||
|
|
||||||
guard.setUserContext(user);
|
expect(guard.granted("read", user)).toBe(true);
|
||||||
|
expect(guard.granted("write", user)).toBe(true);
|
||||||
expect(guard.granted("read")).toBe(true);
|
|
||||||
expect(guard.granted("write")).toBe(true);
|
|
||||||
|
|
||||||
expect(() => guard.granted("something")).toThrow();
|
expect(() => guard.granted("something")).toThrow();
|
||||||
});
|
});
|
||||||
@@ -29,27 +27,25 @@ describe("authorize", () => {
|
|||||||
["read", "write"],
|
["read", "write"],
|
||||||
{
|
{
|
||||||
admin: {
|
admin: {
|
||||||
permissions: ["read", "write"]
|
permissions: ["read", "write"],
|
||||||
},
|
},
|
||||||
guest: {
|
guest: {
|
||||||
permissions: ["read"],
|
permissions: ["read"],
|
||||||
is_default: true
|
is_default: true,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{ enabled: true }
|
},
|
||||||
|
{ enabled: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(guard.granted("read")).toBe(true);
|
expect(guard.granted("read")).toBe(true);
|
||||||
expect(guard.granted("write")).toBe(false);
|
expect(guard.granted("write")).toBe(false);
|
||||||
|
|
||||||
const user = {
|
const user = {
|
||||||
role: "admin"
|
role: "admin",
|
||||||
};
|
};
|
||||||
|
|
||||||
guard.setUserContext(user);
|
expect(guard.granted("read", user)).toBe(true);
|
||||||
|
expect(guard.granted("write", user)).toBe(true);
|
||||||
expect(guard.granted("read")).toBe(true);
|
|
||||||
expect(guard.granted("write")).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("guard implicit allow", async () => {
|
test("guard implicit allow", async () => {
|
||||||
@@ -62,24 +58,24 @@ describe("authorize", () => {
|
|||||||
test("role implicit allow", async () => {
|
test("role implicit allow", async () => {
|
||||||
const guard = Guard.create(["read", "write"], {
|
const guard = Guard.create(["read", "write"], {
|
||||||
admin: {
|
admin: {
|
||||||
implicit_allow: true
|
implicit_allow: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
guard.setUserContext({
|
const user = {
|
||||||
role: "admin"
|
role: "admin",
|
||||||
});
|
};
|
||||||
|
|
||||||
expect(guard.granted("read")).toBe(true);
|
expect(guard.granted("read", user)).toBe(true);
|
||||||
expect(guard.granted("write")).toBe(true);
|
expect(guard.granted("write", user)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("guard with guest role implicit allow", async () => {
|
test("guard with guest role implicit allow", async () => {
|
||||||
const guard = Guard.create(["read", "write"], {
|
const guard = Guard.create(["read", "write"], {
|
||||||
guest: {
|
guest: {
|
||||||
implicit_allow: true,
|
implicit_allow: true,
|
||||||
is_default: true
|
is_default: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(guard.getUserRole()?.name).toBe("guest");
|
expect(guard.getUserRole()?.name).toBe("guest");
|
||||||
|
|||||||
@@ -3,14 +3,16 @@ import { OAuthStrategy } from "../../../src/auth/authenticate/strategies";
|
|||||||
|
|
||||||
const ALL_TESTS = !!process.env.ALL_TESTS;
|
const ALL_TESTS = !!process.env.ALL_TESTS;
|
||||||
|
|
||||||
|
// @todo: add mock response
|
||||||
describe("OAuthStrategy", async () => {
|
describe("OAuthStrategy", async () => {
|
||||||
const strategy = new OAuthStrategy({
|
return;
|
||||||
|
/*const strategy = new OAuthStrategy({
|
||||||
type: "oidc",
|
type: "oidc",
|
||||||
client: {
|
client: {
|
||||||
client_id: process.env.OAUTH_CLIENT_ID,
|
client_id: process.env.OAUTH_CLIENT_ID!,
|
||||||
client_secret: process.env.OAUTH_CLIENT_SECRET
|
client_secret: process.env.OAUTH_CLIENT_SECRET!,
|
||||||
},
|
},
|
||||||
name: "google"
|
name: "google",
|
||||||
});
|
});
|
||||||
const state = "---";
|
const state = "---";
|
||||||
const redirect_uri = "http://localhost:3000/auth/google/callback";
|
const redirect_uri = "http://localhost:3000/auth/google/callback";
|
||||||
@@ -19,28 +21,29 @@ describe("OAuthStrategy", async () => {
|
|||||||
const config = await strategy.getConfig();
|
const config = await strategy.getConfig();
|
||||||
console.log("config", JSON.stringify(config, null, 2));
|
console.log("config", JSON.stringify(config, null, 2));
|
||||||
|
|
||||||
const request = await strategy.request({
|
|
||||||
redirect_uri,
|
|
||||||
state
|
|
||||||
});
|
|
||||||
|
|
||||||
const server = Bun.serve({
|
const server = Bun.serve({
|
||||||
fetch: async (req) => {
|
fetch: async (req) => {
|
||||||
|
console.log("req", req.method, req.url);
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
if (url.pathname === "/auth/google/callback") {
|
if (url.pathname === "/auth/google/callback") {
|
||||||
console.log("req", req);
|
console.log("req", req);
|
||||||
const user = await strategy.callback(url, {
|
const user = await strategy.callback(url, {
|
||||||
redirect_uri,
|
redirect_uri,
|
||||||
state
|
state,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("---user", user);
|
console.log("---user", user);
|
||||||
}
|
}
|
||||||
return new Response("Bun!");
|
return new Response("Bun!");
|
||||||
}
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const request = await strategy.request({
|
||||||
|
redirect_uri,
|
||||||
|
state,
|
||||||
});
|
});
|
||||||
console.log("request", request);
|
console.log("request", request);
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100000));
|
await new Promise((resolve) => setTimeout(resolve, 100000));
|
||||||
});
|
});*/
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class ReturnEvent extends Event<{ foo: string }, string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this.clone({
|
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));
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
delayed();
|
delayed();
|
||||||
},
|
},
|
||||||
"sync"
|
"sync",
|
||||||
);
|
);
|
||||||
|
|
||||||
// don't allow unknown
|
// don't allow unknown
|
||||||
@@ -70,6 +70,9 @@ describe("EventManager", async () => {
|
|||||||
new SpecialEvent({ foo: "bar" });
|
new SpecialEvent({ foo: "bar" });
|
||||||
new InformationalEvent();
|
new InformationalEvent();
|
||||||
|
|
||||||
|
// execute asyncs
|
||||||
|
await emgr.executeAsyncs();
|
||||||
|
|
||||||
expect(call).toHaveBeenCalledTimes(2);
|
expect(call).toHaveBeenCalledTimes(2);
|
||||||
expect(delayed).toHaveBeenCalled();
|
expect(delayed).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -80,15 +83,11 @@ describe("EventManager", async () => {
|
|||||||
call();
|
call();
|
||||||
return Promise.all(p);
|
return Promise.all(p);
|
||||||
};
|
};
|
||||||
const emgr = new EventManager(
|
const emgr = new EventManager({ InformationalEvent });
|
||||||
{ InformationalEvent },
|
|
||||||
{
|
|
||||||
asyncExecutor
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
emgr.onEvent(InformationalEvent, async () => {});
|
emgr.onEvent(InformationalEvent, async () => {});
|
||||||
await emgr.emit(new InformationalEvent());
|
await emgr.emit(new InformationalEvent());
|
||||||
|
await emgr.executeAsyncs(asyncExecutor);
|
||||||
expect(call).toHaveBeenCalled();
|
expect(call).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -98,8 +97,8 @@ describe("EventManager", async () => {
|
|||||||
const emgr = new EventManager(
|
const emgr = new EventManager(
|
||||||
{ ReturnEvent, InformationalEvent },
|
{ ReturnEvent, InformationalEvent },
|
||||||
{
|
{
|
||||||
onInvalidReturn
|
onInvalidReturn,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// @ts-expect-error InformationalEvent has no return value
|
// @ts-expect-error InformationalEvent has no return value
|
||||||
@@ -111,6 +110,7 @@ describe("EventManager", async () => {
|
|||||||
emgr.onEvent(ReturnEvent, async () => "1", "sync");
|
emgr.onEvent(ReturnEvent, async () => "1", "sync");
|
||||||
emgr.onEvent(ReturnEvent, async () => "0", "sync");
|
emgr.onEvent(ReturnEvent, async () => "0", "sync");
|
||||||
|
|
||||||
|
// @todo: fix this
|
||||||
// @ts-expect-error must be string
|
// @ts-expect-error must be string
|
||||||
emgr.onEvent(ReturnEvent, async () => 0, "sync");
|
emgr.onEvent(ReturnEvent, async () => 0, "sync");
|
||||||
|
|
||||||
@@ -124,6 +124,9 @@ describe("EventManager", async () => {
|
|||||||
const e2 = await emgr.emit(new ReturnEvent({ foo: "bar" }));
|
const e2 = await emgr.emit(new ReturnEvent({ foo: "bar" }));
|
||||||
expect(e2.returned).toBe(true);
|
expect(e2.returned).toBe(true);
|
||||||
expect(e2.params.foo).toBe("bar-1-0");
|
expect(e2.params.foo).toBe("bar-1-0");
|
||||||
|
|
||||||
|
await emgr.executeAsyncs();
|
||||||
|
|
||||||
expect(onInvalidReturn).toHaveBeenCalled();
|
expect(onInvalidReturn).toHaveBeenCalled();
|
||||||
expect(asyncEventCallback).toHaveBeenCalled();
|
expect(asyncEventCallback).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -139,7 +142,7 @@ describe("EventManager", async () => {
|
|||||||
expect(slug).toBe("informational-event");
|
expect(slug).toBe("informational-event");
|
||||||
call();
|
call();
|
||||||
},
|
},
|
||||||
{ mode: "sync", once: true }
|
{ mode: "sync", once: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(emgr.getListeners().length).toBe(1);
|
expect(emgr.getListeners().length).toBe(1);
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ describe("Registry", () => {
|
|||||||
first: {
|
first: {
|
||||||
cls: What,
|
cls: What,
|
||||||
schema: Type.Object({ type: Type.String(), what: Type.String() }),
|
schema: Type.Object({ type: Type.String(), what: Type.String() }),
|
||||||
enabled: true
|
enabled: true,
|
||||||
}
|
},
|
||||||
} satisfies Record<string, Test1>);
|
} satisfies Record<string, Test1>);
|
||||||
|
|
||||||
const item = registry.get("first");
|
const item = registry.get("first");
|
||||||
@@ -42,7 +42,7 @@ describe("Registry", () => {
|
|||||||
registry.add("second", {
|
registry.add("second", {
|
||||||
cls: What2,
|
cls: What2,
|
||||||
schema: second,
|
schema: second,
|
||||||
enabled: true
|
enabled: true,
|
||||||
});
|
});
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(registry.get("second").schema).toEqual(second);
|
expect(registry.get("second").schema).toEqual(second);
|
||||||
@@ -52,7 +52,7 @@ describe("Registry", () => {
|
|||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
cls: NotAllowed,
|
cls: NotAllowed,
|
||||||
schema: third,
|
schema: third,
|
||||||
enabled: true
|
enabled: true,
|
||||||
});
|
});
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(registry.get("third").schema).toEqual(third);
|
expect(registry.get("third").schema).toEqual(third);
|
||||||
@@ -62,7 +62,7 @@ describe("Registry", () => {
|
|||||||
cls: What,
|
cls: What,
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
schema: fourth,
|
schema: fourth,
|
||||||
enabled: true
|
enabled: true,
|
||||||
});
|
});
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(registry.get("fourth").schema).toEqual(fourth);
|
expect(registry.get("fourth").schema).toEqual(fourth);
|
||||||
@@ -75,7 +75,7 @@ describe("Registry", () => {
|
|||||||
return {
|
return {
|
||||||
cls: a,
|
cls: a,
|
||||||
schema: a.prototype.getType(),
|
schema: a.prototype.getType(),
|
||||||
enabled: true
|
enabled: true,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 () => {
|
describe("crypto", async () => {
|
||||||
test("sha256", async () => {
|
test("sha256", async () => {
|
||||||
expect(await hash.sha256("test")).toBe(
|
expect(await hash.sha256("test")).toBe(
|
||||||
"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
|
"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
test("sha1", async () => {
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,8 +8,8 @@ describe("SchemaObject", async () => {
|
|||||||
Type.Object({ a: Type.String({ default: "b" }) }),
|
Type.Object({ a: Type.String({ default: "b" }) }),
|
||||||
{ a: "test" },
|
{ a: "test" },
|
||||||
{
|
{
|
||||||
forceParse: true
|
forceParse: true,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(m.get()).toEqual({ a: "test" });
|
expect(m.get()).toEqual({ a: "test" });
|
||||||
@@ -30,14 +30,14 @@ describe("SchemaObject", async () => {
|
|||||||
b: Type.Object(
|
b: Type.Object(
|
||||||
{
|
{
|
||||||
c: Type.String({ default: "d" }),
|
c: Type.String({ default: "d" }),
|
||||||
e: Type.String({ default: "f" })
|
e: Type.String({ default: "f" }),
|
||||||
},
|
},
|
||||||
{ default: {} }
|
{ default: {} },
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{ default: {}, additionalProperties: false }
|
{ default: {}, additionalProperties: false },
|
||||||
)
|
),
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
expect(m.get()).toEqual({ s: { a: "b", b: { c: "d", e: "f" } } });
|
expect(m.get()).toEqual({ s: { a: "b", b: { c: "d", e: "f" } } });
|
||||||
|
|
||||||
@@ -59,8 +59,8 @@ describe("SchemaObject", async () => {
|
|||||||
test("patch array", async () => {
|
test("patch array", async () => {
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
Type.Object({
|
Type.Object({
|
||||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] })
|
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
|
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
|
||||||
|
|
||||||
@@ -81,14 +81,14 @@ describe("SchemaObject", async () => {
|
|||||||
a: Type.String({ default: "b" }),
|
a: Type.String({ default: "b" }),
|
||||||
b: Type.Object(
|
b: Type.Object(
|
||||||
{
|
{
|
||||||
c: Type.String({ default: "d" })
|
c: Type.String({ default: "d" }),
|
||||||
},
|
},
|
||||||
{ default: {} }
|
{ default: {} },
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{ default: {} }
|
{ default: {} },
|
||||||
)
|
),
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
expect(m.get()).toEqual({ s: { a: "b", b: { c: "d" } } });
|
expect(m.get()).toEqual({ s: { a: "b", b: { c: "d" } } });
|
||||||
|
|
||||||
@@ -108,8 +108,8 @@ describe("SchemaObject", async () => {
|
|||||||
test("set", async () => {
|
test("set", async () => {
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
Type.Object({
|
Type.Object({
|
||||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] })
|
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
|
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ describe("SchemaObject", async () => {
|
|||||||
let result: any;
|
let result: any;
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
Type.Object({
|
Type.Object({
|
||||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] })
|
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
|
||||||
}),
|
}),
|
||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
@@ -133,8 +133,8 @@ describe("SchemaObject", async () => {
|
|||||||
await new Promise((r) => setTimeout(r, 10));
|
await new Promise((r) => setTimeout(r, 10));
|
||||||
called = true;
|
called = true;
|
||||||
result = config;
|
result = config;
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await m.set({ methods: ["GET", "POST"] });
|
await m.set({ methods: ["GET", "POST"] });
|
||||||
@@ -146,7 +146,7 @@ describe("SchemaObject", async () => {
|
|||||||
let called = false;
|
let called = false;
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
Type.Object({
|
Type.Object({
|
||||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] })
|
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
|
||||||
}),
|
}),
|
||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
@@ -155,8 +155,8 @@ describe("SchemaObject", async () => {
|
|||||||
called = true;
|
called = true;
|
||||||
to.methods.push("OPTIONS");
|
to.methods.push("OPTIONS");
|
||||||
return to;
|
return to;
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await m.set({ methods: ["GET", "POST"] });
|
const result = await m.set({ methods: ["GET", "POST"] });
|
||||||
@@ -168,7 +168,7 @@ describe("SchemaObject", async () => {
|
|||||||
|
|
||||||
test("throwIfRestricted", async () => {
|
test("throwIfRestricted", async () => {
|
||||||
const m = new SchemaObject(Type.Object({}), undefined, {
|
const m = new SchemaObject(Type.Object({}), undefined, {
|
||||||
restrictPaths: ["a.b"]
|
restrictPaths: ["a.b"],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(() => m.throwIfRestricted("a.b")).toThrow();
|
expect(() => m.throwIfRestricted("a.b")).toThrow();
|
||||||
@@ -185,18 +185,18 @@ describe("SchemaObject", async () => {
|
|||||||
a: Type.String({ default: "b" }),
|
a: Type.String({ default: "b" }),
|
||||||
b: Type.Object(
|
b: Type.Object(
|
||||||
{
|
{
|
||||||
c: Type.String({ default: "d" })
|
c: Type.String({ default: "d" }),
|
||||||
},
|
},
|
||||||
{ default: {} }
|
{ default: {} },
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{ default: {} }
|
{ default: {} },
|
||||||
)
|
),
|
||||||
}),
|
}),
|
||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
restrictPaths: ["s.b"]
|
restrictPaths: ["s.b"],
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(m.patch("s.b.c", "e")).rejects.toThrow();
|
expect(m.patch("s.b.c", "e")).rejects.toThrow();
|
||||||
@@ -217,33 +217,33 @@ describe("SchemaObject", async () => {
|
|||||||
additionalProperties: Type.Object({
|
additionalProperties: Type.Object({
|
||||||
type: Type.String(),
|
type: Type.String(),
|
||||||
config: Type.Optional(
|
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 () => {
|
test("patch safe object, overwrite", async () => {
|
||||||
const data = {
|
const data = {
|
||||||
entities: {
|
entities: {
|
||||||
some: {
|
some: {
|
||||||
fields: {
|
fields: {
|
||||||
a: { type: "string", config: { some: "thing" } }
|
a: { type: "string", config: { some: "thing" } },
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
const m = new SchemaObject(dataEntitiesSchema, data, {
|
const m = new SchemaObject(dataEntitiesSchema, data, {
|
||||||
forceParse: true,
|
forceParse: true,
|
||||||
overwritePaths: [/^entities\..*\.fields\..*\.config/]
|
overwritePaths: [/^entities\..*\.fields\..*\.config/],
|
||||||
});
|
});
|
||||||
|
|
||||||
await m.patch("entities.some.fields.a", { type: "string", config: { another: "one" } });
|
await m.patch("entities.some.fields.a", { type: "string", config: { another: "one" } });
|
||||||
@@ -252,10 +252,10 @@ describe("SchemaObject", async () => {
|
|||||||
entities: {
|
entities: {
|
||||||
some: {
|
some: {
|
||||||
fields: {
|
fields: {
|
||||||
a: { type: "string", config: { another: "one" } }
|
a: { type: "string", config: { another: "one" } },
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -265,22 +265,22 @@ describe("SchemaObject", async () => {
|
|||||||
users: {
|
users: {
|
||||||
fields: {
|
fields: {
|
||||||
email: { type: "string" },
|
email: { type: "string" },
|
||||||
password: { type: "string" }
|
password: { type: "string" },
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
const m = new SchemaObject(dataEntitiesSchema, data, {
|
const m = new SchemaObject(dataEntitiesSchema, data, {
|
||||||
forceParse: true,
|
forceParse: true,
|
||||||
overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/]
|
overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/],
|
||||||
});
|
});
|
||||||
|
|
||||||
await m.patch("entities.test", {
|
await m.patch("entities.test", {
|
||||||
fields: {
|
fields: {
|
||||||
content: {
|
content: {
|
||||||
type: "text"
|
type: "text",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(m.get()).toEqual({
|
expect(m.get()).toEqual({
|
||||||
@@ -288,17 +288,17 @@ describe("SchemaObject", async () => {
|
|||||||
users: {
|
users: {
|
||||||
fields: {
|
fields: {
|
||||||
email: { type: "string" },
|
email: { type: "string" },
|
||||||
password: { type: "string" }
|
password: { type: "string" },
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
fields: {
|
fields: {
|
||||||
content: {
|
content: {
|
||||||
type: "text"
|
type: "text",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -308,14 +308,14 @@ describe("SchemaObject", async () => {
|
|||||||
users: {
|
users: {
|
||||||
fields: {
|
fields: {
|
||||||
email: { type: "string" },
|
email: { type: "string" },
|
||||||
password: { type: "string" }
|
password: { type: "string" },
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
const m = new SchemaObject(dataEntitiesSchema, data, {
|
const m = new SchemaObject(dataEntitiesSchema, data, {
|
||||||
forceParse: true,
|
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();
|
expect(m.patch("desc", "entities.users.config.sort_dir")).rejects.toThrow();
|
||||||
@@ -323,13 +323,13 @@ describe("SchemaObject", async () => {
|
|||||||
await m.patch("entities.test", {
|
await m.patch("entities.test", {
|
||||||
fields: {
|
fields: {
|
||||||
content: {
|
content: {
|
||||||
type: "text"
|
type: "text",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await m.patch("entities.users.config", {
|
await m.patch("entities.users.config", {
|
||||||
sort_dir: "desc"
|
sort_dir: "desc",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(m.get()).toEqual({
|
expect(m.get()).toEqual({
|
||||||
@@ -337,20 +337,20 @@ describe("SchemaObject", async () => {
|
|||||||
users: {
|
users: {
|
||||||
fields: {
|
fields: {
|
||||||
email: { type: "string" },
|
email: { type: "string" },
|
||||||
password: { type: "string" }
|
password: { type: "string" },
|
||||||
},
|
},
|
||||||
config: {
|
config: {
|
||||||
sort_dir: "desc"
|
sort_dir: "desc",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
fields: {
|
fields: {
|
||||||
content: {
|
content: {
|
||||||
type: "text"
|
type: "text",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ describe("diff", () => {
|
|||||||
t: "a",
|
t: "a",
|
||||||
p: ["b"],
|
p: ["b"],
|
||||||
o: undefined,
|
o: undefined,
|
||||||
n: 2
|
n: 2,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -35,8 +35,8 @@ describe("diff", () => {
|
|||||||
t: "r",
|
t: "r",
|
||||||
p: ["b"],
|
p: ["b"],
|
||||||
o: 2,
|
o: 2,
|
||||||
n: undefined
|
n: undefined,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -57,8 +57,8 @@ describe("diff", () => {
|
|||||||
t: "e",
|
t: "e",
|
||||||
p: ["a"],
|
p: ["a"],
|
||||||
o: 1,
|
o: 1,
|
||||||
n: 2
|
n: 2,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -79,8 +79,8 @@ describe("diff", () => {
|
|||||||
t: "e",
|
t: "e",
|
||||||
p: ["a", "b"],
|
p: ["a", "b"],
|
||||||
o: 1,
|
o: 1,
|
||||||
n: 2
|
n: 2,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -101,14 +101,14 @@ describe("diff", () => {
|
|||||||
t: "e",
|
t: "e",
|
||||||
p: ["a", 1],
|
p: ["a", 1],
|
||||||
o: 2,
|
o: 2,
|
||||||
n: 4
|
n: 4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
t: "a",
|
t: "a",
|
||||||
p: ["a", 3],
|
p: ["a", 3],
|
||||||
o: undefined,
|
o: undefined,
|
||||||
n: 5
|
n: 5,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -129,20 +129,20 @@ describe("diff", () => {
|
|||||||
t: "a",
|
t: "a",
|
||||||
p: ["a", 0],
|
p: ["a", 0],
|
||||||
o: undefined,
|
o: undefined,
|
||||||
n: 1
|
n: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
t: "a",
|
t: "a",
|
||||||
p: ["a", 1],
|
p: ["a", 1],
|
||||||
o: undefined,
|
o: undefined,
|
||||||
n: 2
|
n: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
t: "a",
|
t: "a",
|
||||||
p: ["a", 2],
|
p: ["a", 2],
|
||||||
o: undefined,
|
o: undefined,
|
||||||
n: 3
|
n: 3,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -163,14 +163,14 @@ describe("diff", () => {
|
|||||||
t: "e",
|
t: "e",
|
||||||
p: ["a", 1],
|
p: ["a", 1],
|
||||||
o: 2,
|
o: 2,
|
||||||
n: 3
|
n: 3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
t: "r",
|
t: "r",
|
||||||
p: ["a", 2],
|
p: ["a", 2],
|
||||||
o: 3,
|
o: 3,
|
||||||
n: undefined
|
n: undefined,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -183,14 +183,14 @@ describe("diff", () => {
|
|||||||
it("should handle complex nested changes", () => {
|
it("should handle complex nested changes", () => {
|
||||||
const oldObj = {
|
const oldObj = {
|
||||||
a: {
|
a: {
|
||||||
b: [1, 2, { c: 3 }]
|
b: [1, 2, { c: 3 }],
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const newObj = {
|
const newObj = {
|
||||||
a: {
|
a: {
|
||||||
b: [1, 2, { c: 4 }, 5]
|
b: [1, 2, { c: 4 }, 5],
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const diffs = diff(oldObj, newObj);
|
const diffs = diff(oldObj, newObj);
|
||||||
@@ -200,14 +200,14 @@ describe("diff", () => {
|
|||||||
t: "e",
|
t: "e",
|
||||||
p: ["a", "b", 2, "c"],
|
p: ["a", "b", 2, "c"],
|
||||||
o: 3,
|
o: 3,
|
||||||
n: 4
|
n: 4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
t: "a",
|
t: "a",
|
||||||
p: ["a", "b", 3],
|
p: ["a", "b", 3],
|
||||||
o: undefined,
|
o: undefined,
|
||||||
n: 5
|
n: 5,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -228,14 +228,14 @@ describe("diff", () => {
|
|||||||
t: "e",
|
t: "e",
|
||||||
p: ["a"],
|
p: ["a"],
|
||||||
o: undefined,
|
o: undefined,
|
||||||
n: null
|
n: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
t: "e",
|
t: "e",
|
||||||
p: ["b"],
|
p: ["b"],
|
||||||
o: null,
|
o: null,
|
||||||
n: undefined
|
n: undefined,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -256,8 +256,8 @@ describe("diff", () => {
|
|||||||
t: "e",
|
t: "e",
|
||||||
p: ["a"],
|
p: ["a"],
|
||||||
o: 1,
|
o: 1,
|
||||||
n: "1"
|
n: "1",
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -278,14 +278,14 @@ describe("diff", () => {
|
|||||||
t: "r",
|
t: "r",
|
||||||
p: ["b"],
|
p: ["b"],
|
||||||
o: 2,
|
o: 2,
|
||||||
n: undefined
|
n: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
t: "a",
|
t: "a",
|
||||||
p: ["c"],
|
p: ["c"],
|
||||||
o: undefined,
|
o: undefined,
|
||||||
n: 3
|
n: 3,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -306,8 +306,8 @@ describe("diff", () => {
|
|||||||
t: "e",
|
t: "e",
|
||||||
p: ["a"],
|
p: ["a"],
|
||||||
o: [1, 2, 3],
|
o: [1, 2, 3],
|
||||||
n: { b: 4 }
|
n: { b: 4 },
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -328,8 +328,8 @@ describe("diff", () => {
|
|||||||
t: "e",
|
t: "e",
|
||||||
p: ["a"],
|
p: ["a"],
|
||||||
o: { b: 1 },
|
o: { b: 1 },
|
||||||
n: 2
|
n: 2,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -350,14 +350,14 @@ describe("diff", () => {
|
|||||||
t: "r",
|
t: "r",
|
||||||
p: ["a"],
|
p: ["a"],
|
||||||
o: 1,
|
o: 1,
|
||||||
n: undefined
|
n: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
t: "a",
|
t: "a",
|
||||||
p: ["b"],
|
p: ["b"],
|
||||||
o: undefined,
|
o: undefined,
|
||||||
n: 2
|
n: 2,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -408,8 +408,8 @@ describe("diff", () => {
|
|||||||
t: "a",
|
t: "a",
|
||||||
p: ["a"],
|
p: ["a"],
|
||||||
o: undefined,
|
o: undefined,
|
||||||
n: 1
|
n: 1,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -430,8 +430,8 @@ describe("diff", () => {
|
|||||||
t: "r",
|
t: "r",
|
||||||
p: ["a"],
|
p: ["a"],
|
||||||
o: 1,
|
o: 1,
|
||||||
n: undefined
|
n: undefined,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ describe("object-query", () => {
|
|||||||
|
|
||||||
test("validates", async () => {
|
test("validates", async () => {
|
||||||
const converted = convert({
|
const converted = convert({
|
||||||
name: { $eq: "ch" }
|
name: { $eq: "ch" },
|
||||||
});
|
});
|
||||||
validate(converted, { name: "Michael" });
|
validate(converted, { name: "Michael" });
|
||||||
});
|
});
|
||||||
@@ -31,7 +31,7 @@ describe("object-query", () => {
|
|||||||
[{ val: { $notnull: 1 } }, { val: null }, false],
|
[{ val: { $notnull: 1 } }, { val: null }, false],
|
||||||
[{ val: { $regex: ".*" } }, { val: "test" }, true],
|
[{ val: { $regex: ".*" } }, { val: "test" }, true],
|
||||||
[{ val: { $regex: /^t.*/ } }, { 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) {
|
for (const [query, object, expected] of tests) {
|
||||||
@@ -55,10 +55,10 @@ describe("object-query", () => {
|
|||||||
[
|
[
|
||||||
{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } },
|
{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } },
|
||||||
{ val1: "foo", val2: "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: 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) {
|
for (const [query, object, expected] of tests) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Perf } from "../../src/core/utils";
|
import { Perf, ucFirst } from "../../src/core/utils";
|
||||||
import * as utils from "../../src/core/utils";
|
import * as utils from "../../src/core/utils";
|
||||||
|
import { assetsPath } from "../helper";
|
||||||
|
|
||||||
async function wait(ms: number) {
|
async function wait(ms: number) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@@ -16,7 +17,7 @@ describe("Core Utils", async () => {
|
|||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{ key: "a", value: 1 },
|
{ key: "a", value: 1 },
|
||||||
{ key: "b", value: 2 },
|
{ 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);
|
const obj = utils.headersToObject(headers);
|
||||||
expect(obj).toEqual({
|
expect(obj).toEqual({
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
authorization: "Bearer 123"
|
authorization: "Bearer 123",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -115,7 +116,7 @@ describe("Core Utils", async () => {
|
|||||||
[{ a: 1, b: 2, c: 3 }, ["b"], { a: 1, c: 3 }],
|
[{ 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 }, ["c"], { a: 1, b: 2 }],
|
||||||
[{ a: 1, b: 2, c: 3 }, ["a", "b"], { c: 3 }],
|
[{ 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][];
|
] as [object, string[], object][];
|
||||||
|
|
||||||
for (const [obj, keys, expected] of objects) {
|
for (const [obj, keys, expected] of objects) {
|
||||||
@@ -134,8 +135,8 @@ describe("Core Utils", async () => {
|
|||||||
[true, true, true],
|
[true, true, true],
|
||||||
[true, false, false],
|
[true, false, false],
|
||||||
[false, false, true],
|
[false, false, true],
|
||||||
[1, NaN, false],
|
[1, Number.NaN, false],
|
||||||
[NaN, NaN, true],
|
[Number.NaN, Number.NaN, true],
|
||||||
[null, null, true],
|
[null, null, true],
|
||||||
[null, undefined, false],
|
[null, undefined, false],
|
||||||
[undefined, undefined, true],
|
[undefined, undefined, true],
|
||||||
@@ -146,9 +147,9 @@ describe("Core Utils", async () => {
|
|||||||
new Map([["a", 1]]),
|
new Map([["a", 1]]),
|
||||||
new Map([
|
new Map([
|
||||||
["a", 1],
|
["a", 1],
|
||||||
["b", 2]
|
["b", 2],
|
||||||
]),
|
]),
|
||||||
false
|
false,
|
||||||
],
|
],
|
||||||
[{ a: 1 }, { a: 1 }, true],
|
[{ a: 1 }, { a: 1 }, true],
|
||||||
[{ a: 1 }, { a: 2 }, false],
|
[{ a: 1 }, { a: 2 }, false],
|
||||||
@@ -169,7 +170,7 @@ describe("Core Utils", async () => {
|
|||||||
[[1, 2, 3], [1, 2, 3, 4], false],
|
[[1, 2, 3], [1, 2, 3, 4], false],
|
||||||
[[{ a: 1 }], [{ a: 1 }], true],
|
[[{ a: 1 }], [{ a: 1 }], true],
|
||||||
[[{ a: 1 }], [{ a: 2 }], false],
|
[[{ a: 1 }], [{ a: 2 }], false],
|
||||||
[[{ a: 1 }], [{ b: 1 }], false]
|
[[{ a: 1 }], [{ b: 1 }], false],
|
||||||
] as [any, any, boolean][];
|
] as [any, any, boolean][];
|
||||||
|
|
||||||
for (const [a, b, expected] of objects) {
|
for (const [a, b, expected] of objects) {
|
||||||
@@ -185,7 +186,7 @@ describe("Core Utils", async () => {
|
|||||||
[{ a: { b: 1 } }, "a.b", 1],
|
[{ a: { b: 1 } }, "a.b", 1],
|
||||||
[{ a: { b: 1 } }, "a.b.c", null, null],
|
[{ a: { b: 1 } }, "a.b.c", null, null],
|
||||||
[{ a: { b: 1 } }, "a.b.c", 1, 1],
|
[{ a: { b: 1 } }, "a.b.c", 1, 1],
|
||||||
[[[1]], "0.0", 1]
|
[[[1]], "0.0", 1],
|
||||||
] as [object, string, any, any][];
|
] as [object, string, any, any][];
|
||||||
|
|
||||||
for (const [obj, path, expected, defaultValue] of tests) {
|
for (const [obj, path, expected, defaultValue] of tests) {
|
||||||
@@ -194,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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
ManyToOneRelation,
|
ManyToOneRelation,
|
||||||
type MutatorResponse,
|
type MutatorResponse,
|
||||||
type RepositoryResponse,
|
type RepositoryResponse,
|
||||||
TextField
|
TextField,
|
||||||
} from "../../src/data";
|
} from "../../src/data";
|
||||||
import { DataController } from "../../src/data/api/DataController";
|
import { DataController } from "../../src/data/api/DataController";
|
||||||
import { dataConfigSchema } from "../../src/data/data-schema";
|
import { dataConfigSchema } from "../../src/data/data-schema";
|
||||||
@@ -35,17 +35,17 @@ describe("[data] DataController", async () => {
|
|||||||
meta: {
|
meta: {
|
||||||
total: 0,
|
total: 0,
|
||||||
count: 0,
|
count: 0,
|
||||||
items: 0
|
items: 0,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(res).toEqual({
|
expect(res).toEqual({
|
||||||
meta: {
|
meta: {
|
||||||
total: 0,
|
total: 0,
|
||||||
count: 0,
|
count: 0,
|
||||||
items: 0
|
items: 0,
|
||||||
},
|
},
|
||||||
data: []
|
data: [],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -59,22 +59,22 @@ describe("[data] DataController", async () => {
|
|||||||
data: [] as any,
|
data: [] as any,
|
||||||
sql: "",
|
sql: "",
|
||||||
parameters: [] as any,
|
parameters: [] as any,
|
||||||
result: [] as any
|
result: [] as any,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(res).toEqual({
|
expect(res).toEqual({
|
||||||
data: []
|
data: [],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getController", async () => {
|
describe("getController", async () => {
|
||||||
const users = new Entity("users", [
|
const users = new Entity("users", [
|
||||||
new TextField("name", { required: true }),
|
new TextField("name", { required: true }),
|
||||||
new TextField("bio")
|
new TextField("bio"),
|
||||||
]);
|
]);
|
||||||
const posts = new Entity("posts", [new TextField("content")]);
|
const posts = new Entity("posts", [new TextField("content")]);
|
||||||
const em = new EntityManager([users, posts], dummyConnection, [
|
const em = new EntityManager([users, posts], dummyConnection, [
|
||||||
new ManyToOneRelation(posts, users)
|
new ManyToOneRelation(posts, users),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await em.schema().sync({ force: true });
|
await em.schema().sync({ force: true });
|
||||||
@@ -83,12 +83,12 @@ describe("[data] DataController", async () => {
|
|||||||
users: [
|
users: [
|
||||||
{ name: "foo", bio: "bar" },
|
{ name: "foo", bio: "bar" },
|
||||||
{ name: "bar", bio: null },
|
{ name: "bar", bio: null },
|
||||||
{ name: "baz", bio: "!!!" }
|
{ name: "baz", bio: "!!!" },
|
||||||
],
|
],
|
||||||
posts: [
|
posts: [
|
||||||
{ content: "post 1", users_id: 1 },
|
{ 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() };
|
const ctx: any = { em, guard: new Guard() };
|
||||||
@@ -116,9 +116,9 @@ describe("[data] DataController", async () => {
|
|||||||
//console.log("app.routes", app.routes);
|
//console.log("app.routes", app.routes);
|
||||||
// create users
|
// create users
|
||||||
for await (const _user of fixtures.users) {
|
for await (const _user of fixtures.users) {
|
||||||
const res = await app.request("/users", {
|
const res = await app.request("/entity/users", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(_user)
|
body: JSON.stringify(_user),
|
||||||
});
|
});
|
||||||
//console.log("res", { _user }, res);
|
//console.log("res", { _user }, res);
|
||||||
const result = (await res.json()) as MutatorResponse;
|
const result = (await res.json()) as MutatorResponse;
|
||||||
@@ -131,9 +131,9 @@ describe("[data] DataController", async () => {
|
|||||||
|
|
||||||
// create posts
|
// create posts
|
||||||
for await (const _post of fixtures.posts) {
|
for await (const _post of fixtures.posts) {
|
||||||
const res = await app.request("/posts", {
|
const res = await app.request("/entity/posts", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(_post)
|
body: JSON.stringify(_post),
|
||||||
});
|
});
|
||||||
const result = (await res.json()) as MutatorResponse;
|
const result = (await res.json()) as MutatorResponse;
|
||||||
const { id, ...data } = result.data as any;
|
const { id, ...data } = result.data as any;
|
||||||
@@ -145,7 +145,7 @@ describe("[data] DataController", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("/:entity (read many)", async () => {
|
test("/:entity (read many)", async () => {
|
||||||
const res = await app.request("/users");
|
const res = await app.request("/entity/users");
|
||||||
const data = (await res.json()) as RepositoryResponse;
|
const data = (await res.json()) as RepositoryResponse;
|
||||||
|
|
||||||
expect(data.meta.total).toBe(3);
|
expect(data.meta.total).toBe(3);
|
||||||
@@ -156,14 +156,14 @@ describe("[data] DataController", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("/:entity/query (func query)", async () => {
|
test("/:entity/query (func query)", async () => {
|
||||||
const res = await app.request("/users/query", {
|
const res = await app.request("/entity/users/query", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
where: { bio: { $isnull: 1 } }
|
where: { bio: { $isnull: 1 } },
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
const data = (await res.json()) as RepositoryResponse;
|
const data = (await res.json()) as RepositoryResponse;
|
||||||
|
|
||||||
@@ -175,7 +175,7 @@ describe("[data] DataController", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("/:entity (read many, paginated)", async () => {
|
test("/:entity (read many, paginated)", async () => {
|
||||||
const res = await app.request("/users?limit=1&offset=2");
|
const res = await app.request("/entity/users?limit=1&offset=2");
|
||||||
const data = (await res.json()) as RepositoryResponse;
|
const data = (await res.json()) as RepositoryResponse;
|
||||||
|
|
||||||
expect(data.meta.total).toBe(3);
|
expect(data.meta.total).toBe(3);
|
||||||
@@ -186,7 +186,7 @@ describe("[data] DataController", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("/:entity/:id (read one)", async () => {
|
test("/:entity/:id (read one)", async () => {
|
||||||
const res = await app.request("/users/3");
|
const res = await app.request("/entity/users/3");
|
||||||
const data = (await res.json()) as RepositoryResponse<EntityData>;
|
const data = (await res.json()) as RepositoryResponse<EntityData>;
|
||||||
console.log("data", data);
|
console.log("data", data);
|
||||||
|
|
||||||
@@ -197,9 +197,9 @@ describe("[data] DataController", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("/:entity (update one)", async () => {
|
test("/:entity (update one)", async () => {
|
||||||
const res = await app.request("/users/3", {
|
const res = await app.request("/entity/users/3", {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: JSON.stringify({ name: "new name" })
|
body: JSON.stringify({ name: "new name" }),
|
||||||
});
|
});
|
||||||
const { data } = (await res.json()) as MutatorResponse;
|
const { data } = (await res.json()) as MutatorResponse;
|
||||||
|
|
||||||
@@ -208,7 +208,7 @@ describe("[data] DataController", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("/:entity/:id/:reference (read references)", async () => {
|
test("/:entity/:id/:reference (read references)", async () => {
|
||||||
const res = await app.request("/users/1/posts");
|
const res = await app.request("/entity/users/1/posts");
|
||||||
const data = (await res.json()) as RepositoryResponse;
|
const data = (await res.json()) as RepositoryResponse;
|
||||||
console.log("data", data);
|
console.log("data", data);
|
||||||
|
|
||||||
@@ -220,14 +220,14 @@ describe("[data] DataController", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("/:entity/:id (delete one)", async () => {
|
test("/:entity/:id (delete one)", async () => {
|
||||||
const res = await app.request("/posts/2", {
|
const res = await app.request("/entity/posts/2", {
|
||||||
method: "DELETE"
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
const { data } = (await res.json()) as RepositoryResponse<EntityData>;
|
const { data } = (await res.json()) as RepositoryResponse<EntityData>;
|
||||||
expect(data).toEqual({ id: 2, ...fixtures.posts[1] });
|
expect(data).toEqual({ id: 2, ...fixtures.posts[1] });
|
||||||
|
|
||||||
// verify
|
// verify
|
||||||
const res2 = await app.request("/posts");
|
const res2 = await app.request("/entity/posts");
|
||||||
const data2 = (await res2.json()) as RepositoryResponse;
|
const data2 = (await res2.json()) as RepositoryResponse;
|
||||||
expect(data2.meta.total).toBe(1);
|
expect(data2.meta.total).toBe(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ describe("data-query-impl", () => {
|
|||||||
[{ val: { $isnull: 0 } }, '"val" is not null', []],
|
[{ val: { $isnull: 0 } }, '"val" is not null', []],
|
||||||
[{ val: { $isnull: false } }, '"val" is not null', []],
|
[{ val: { $isnull: false } }, '"val" is not null', []],
|
||||||
[{ val: { $like: "what" } }, '"val" like ?', ["what"]],
|
[{ val: { $like: "what" } }, '"val" like ?', ["what"]],
|
||||||
[{ val: { $like: "w*t" } }, '"val" like ?', ["w%t"]]
|
[{ val: { $like: "w*t" } }, '"val" like ?', ["w%t"]],
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const [query, expectedSql, expectedParams] of tests) {
|
for (const [query, expectedSql, expectedParams] of tests) {
|
||||||
@@ -51,22 +51,22 @@ describe("data-query-impl", () => {
|
|||||||
[
|
[
|
||||||
{ val1: { $eq: "foo" }, val2: { $eq: "bar" } },
|
{ val1: { $eq: "foo" }, val2: { $eq: "bar" } },
|
||||||
'("val1" = ? and "val2" = ?)',
|
'("val1" = ? and "val2" = ?)',
|
||||||
["foo", "bar"]
|
["foo", "bar"],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{ val1: { $eq: "foo" }, val2: { $eq: "bar" } },
|
{ val1: { $eq: "foo" }, val2: { $eq: "bar" } },
|
||||||
'("val1" = ? and "val2" = ?)',
|
'("val1" = ? and "val2" = ?)',
|
||||||
["foo", "bar"]
|
["foo", "bar"],
|
||||||
],
|
],
|
||||||
|
|
||||||
// or constructs
|
// or constructs
|
||||||
[
|
[
|
||||||
{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } },
|
{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } },
|
||||||
'("val1" = ? or "val2" = ?)',
|
'("val1" = ? or "val2" = ?)',
|
||||||
["foo", "bar"]
|
["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]],
|
||||||
[{ 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) {
|
for (const [query, expectedSql, expectedParams] of tests) {
|
||||||
@@ -86,7 +86,7 @@ describe("data-query-impl", () => {
|
|||||||
|
|
||||||
// or constructs
|
// or constructs
|
||||||
[{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } }, ["val1", "val2"]],
|
[{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } }, ["val1", "val2"]],
|
||||||
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, ["val1"]]
|
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, ["val1"]],
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const [query, expectedKeys] of tests) {
|
for (const [query, expectedKeys] of tests) {
|
||||||
@@ -105,23 +105,23 @@ describe("data-query-impl", () => {
|
|||||||
posts: {
|
posts: {
|
||||||
with: {
|
with: {
|
||||||
images: {
|
images: {
|
||||||
select: ["id"]
|
select: ["id"],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
with: {
|
with: {
|
||||||
posts: {
|
posts: {
|
||||||
with: {
|
with: {
|
||||||
images: {
|
images: {
|
||||||
select: ["id"]
|
select: ["id"],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// over http
|
// over http
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
NumberField,
|
NumberField,
|
||||||
PrimaryField,
|
PrimaryField,
|
||||||
Repository,
|
Repository,
|
||||||
TextField
|
TextField,
|
||||||
} from "../../src/data";
|
} from "../../src/data";
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
|
|
||||||
@@ -18,14 +18,14 @@ describe("some tests", async () => {
|
|||||||
|
|
||||||
const users = new Entity("users", [
|
const users = new Entity("users", [
|
||||||
new TextField("username", { required: true, default_value: "nobody" }),
|
new TextField("username", { required: true, default_value: "nobody" }),
|
||||||
new TextField("email", { maxLength: 3 })
|
new TextField("email", { maxLength: 3 }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const posts = new Entity("posts", [
|
const posts = new Entity("posts", [
|
||||||
new TextField("title"),
|
new TextField("title"),
|
||||||
new TextField("content"),
|
new TextField("content"),
|
||||||
new TextField("created_at"),
|
new TextField("created_at"),
|
||||||
new NumberField("likes", { default_value: 0 })
|
new NumberField("likes", { default_value: 0 }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const em = new EntityManager([users, posts], connection);
|
const em = new EntityManager([users, posts], connection);
|
||||||
@@ -43,7 +43,7 @@ describe("some tests", async () => {
|
|||||||
});*/
|
});*/
|
||||||
|
|
||||||
expect(query.sql).toBe(
|
expect(query.sql).toBe(
|
||||||
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" where "id" = ? limit ?'
|
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" where "id" = ? limit ?',
|
||||||
);
|
);
|
||||||
expect(query.parameters).toEqual([1, 1]);
|
expect(query.parameters).toEqual([1, 1]);
|
||||||
expect(query.result).toEqual([]);
|
expect(query.result).toEqual([]);
|
||||||
@@ -53,7 +53,7 @@ describe("some tests", async () => {
|
|||||||
const query = await em.repository(users).findMany();
|
const query = await em.repository(users).findMany();
|
||||||
|
|
||||||
expect(query.sql).toBe(
|
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.parameters).toEqual([10, 0]);
|
||||||
expect(query.result).toEqual([]);
|
expect(query.result).toEqual([]);
|
||||||
@@ -63,7 +63,7 @@ describe("some tests", async () => {
|
|||||||
const query = await em.repository(posts).findMany();
|
const query = await em.repository(posts).findMany();
|
||||||
|
|
||||||
expect(query.sql).toBe(
|
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.parameters).toEqual([10, 0]);
|
||||||
expect(query.result).toEqual([]);
|
expect(query.result).toEqual([]);
|
||||||
@@ -74,7 +74,7 @@ describe("some tests", async () => {
|
|||||||
new Entity("users", [
|
new Entity("users", [
|
||||||
new TextField("username"),
|
new TextField("username"),
|
||||||
new TextField("email"),
|
new TextField("email"),
|
||||||
new TextField("email") // not throwing, it's just being ignored
|
new TextField("email"), // not throwing, it's just being ignored
|
||||||
]);
|
]);
|
||||||
}).toBeDefined();
|
}).toBeDefined();
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ describe("some tests", async () => {
|
|||||||
new TextField("username"),
|
new TextField("username"),
|
||||||
new TextField("email"),
|
new TextField("email"),
|
||||||
// field config differs, will throw
|
// field config differs, will throw
|
||||||
new TextField("email", { required: true })
|
new TextField("email", { required: true }),
|
||||||
]);
|
]);
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ describe("some tests", async () => {
|
|||||||
new Entity("users", [
|
new Entity("users", [
|
||||||
new PrimaryField(),
|
new PrimaryField(),
|
||||||
new TextField("username"),
|
new TextField("username"),
|
||||||
new TextField("email")
|
new TextField("email"),
|
||||||
]);
|
]);
|
||||||
}).toBeDefined();
|
}).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export function getDummyDatabase(memory: boolean = true): {
|
|||||||
afterAllCleanup: async () => {
|
afterAllCleanup: async () => {
|
||||||
if (!memory) await unlink(DB_NAME);
|
if (!memory) await unlink(DB_NAME);
|
||||||
return true;
|
return true;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ export function getDummyConnection(memory: boolean = true) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
dummyConnection,
|
dummyConnection,
|
||||||
afterAllCleanup
|
afterAllCleanup,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
ManyToOneRelation,
|
ManyToOneRelation,
|
||||||
NumberField,
|
NumberField,
|
||||||
SchemaManager,
|
SchemaManager,
|
||||||
TextField
|
TextField,
|
||||||
} from "../../src/data";
|
} from "../../src/data";
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ describe("Mutator relation", async () => {
|
|||||||
const posts = new Entity("posts", [
|
const posts = new Entity("posts", [
|
||||||
new TextField("title"),
|
new TextField("title"),
|
||||||
new TextField("content", { default_value: "..." }),
|
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")]);
|
const users = new Entity("users", [new TextField("username")]);
|
||||||
@@ -44,7 +44,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: 10 })).rejects.toThrow();
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
em.mutator(posts).insertOne({ title: "post2", users_id: data.id })
|
em.mutator(posts).insertOne({ title: "post2", users_id: data.id }),
|
||||||
).resolves.toBeDefined();
|
).resolves.toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ describe("Mutator simple", async () => {
|
|||||||
|
|
||||||
const items = new Entity("items", [
|
const items = new Entity("items", [
|
||||||
new TextField("label", { required: true, minLength: 1 }),
|
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);
|
const em = new EntityManager<any>([items], connection);
|
||||||
|
|
||||||
@@ -29,11 +29,11 @@ describe("Mutator simple", async () => {
|
|||||||
test("insert single row", async () => {
|
test("insert single row", async () => {
|
||||||
const mutation = await em.mutator(items).insertOne({
|
const mutation = await em.mutator(items).insertOne({
|
||||||
label: "test",
|
label: "test",
|
||||||
count: 1
|
count: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mutation.sql).toBe(
|
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 });
|
expect(mutation.data).toEqual({ id: 1, label: "test", count: 1 });
|
||||||
|
|
||||||
@@ -41,8 +41,8 @@ describe("Mutator simple", async () => {
|
|||||||
limit: 1,
|
limit: 1,
|
||||||
sort: {
|
sort: {
|
||||||
by: "id",
|
by: "id",
|
||||||
dir: "desc"
|
dir: "desc",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(query.result).toEqual([{ id: 1, label: "test", count: 1 }]);
|
expect(query.result).toEqual([{ id: 1, label: "test", count: 1 }]);
|
||||||
@@ -53,18 +53,18 @@ describe("Mutator simple", async () => {
|
|||||||
limit: 1,
|
limit: 1,
|
||||||
sort: {
|
sort: {
|
||||||
by: "id",
|
by: "id",
|
||||||
dir: "desc"
|
dir: "desc",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
const id = query.data![0].id as number;
|
const id = query.data![0].id as number;
|
||||||
|
|
||||||
const mutation = await em.mutator(items).updateOne(id, {
|
const mutation = await em.mutator(items).updateOne(id, {
|
||||||
label: "new label",
|
label: "new label",
|
||||||
count: 100
|
count: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mutation.sql).toBe(
|
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 });
|
expect(mutation.data).toEqual({ id, label: "new label", count: 100 });
|
||||||
});
|
});
|
||||||
@@ -74,15 +74,15 @@ describe("Mutator simple", async () => {
|
|||||||
limit: 1,
|
limit: 1,
|
||||||
sort: {
|
sort: {
|
||||||
by: "id",
|
by: "id",
|
||||||
dir: "desc"
|
dir: "desc",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const id = query.data![0].id as number;
|
const id = query.data![0].id as number;
|
||||||
const mutation = await em.mutator(items).deleteOne(id);
|
const mutation = await em.mutator(items).deleteOne(id);
|
||||||
|
|
||||||
expect(mutation.sql).toBe(
|
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 });
|
expect(mutation.data).toEqual({ id, label: "new label", count: 100 });
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ describe("Mutator simple", async () => {
|
|||||||
const incompleteCreate = async () =>
|
const incompleteCreate = async () =>
|
||||||
await em.mutator(items).insertOne({
|
await em.mutator(items).insertOne({
|
||||||
//label: "test",
|
//label: "test",
|
||||||
count: 1
|
count: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(incompleteCreate()).rejects.toThrow();
|
expect(incompleteCreate()).rejects.toThrow();
|
||||||
@@ -104,7 +104,7 @@ describe("Mutator simple", async () => {
|
|||||||
const invalidCreate1 = async () =>
|
const invalidCreate1 = async () =>
|
||||||
await em.mutator(items).insertOne({
|
await em.mutator(items).insertOne({
|
||||||
label: 111, // this should work
|
label: 111, // this should work
|
||||||
count: "1" // this should fail
|
count: "1", // this should fail
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(invalidCreate1()).rejects.toThrow(TransformPersistFailedException);
|
expect(invalidCreate1()).rejects.toThrow(TransformPersistFailedException);
|
||||||
@@ -112,7 +112,7 @@ describe("Mutator simple", async () => {
|
|||||||
const invalidCreate2 = async () =>
|
const invalidCreate2 = async () =>
|
||||||
await em.mutator(items).insertOne({
|
await em.mutator(items).insertOne({
|
||||||
label: "", // this should fail
|
label: "", // this should fail
|
||||||
count: 1
|
count: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(invalidCreate2()).rejects.toThrow(TransformPersistFailedException);
|
expect(invalidCreate2()).rejects.toThrow(TransformPersistFailedException);
|
||||||
@@ -137,7 +137,7 @@ describe("Mutator simple", async () => {
|
|||||||
expect((await em.repository(items).findMany()).data.length).toBe(data.length - 2);
|
expect((await em.repository(items).findMany()).data.length).toBe(data.length - 2);
|
||||||
//console.log((await em.repository(items).findMany()).data);
|
//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((await em.repository(items).findMany()).data.length).toBe(0);
|
||||||
|
|
||||||
//expect(res.data.count).toBe(0);
|
//expect(res.data.count).toBe(0);
|
||||||
@@ -152,27 +152,27 @@ describe("Mutator simple", async () => {
|
|||||||
await em.mutator(items).updateWhere(
|
await em.mutator(items).updateWhere(
|
||||||
{ count: 2 },
|
{ count: 2 },
|
||||||
{
|
{
|
||||||
count: 10
|
count: 10,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
expect((await em.repository(items).findMany()).data).toEqual([
|
expect((await em.repository(items).findMany()).data).toEqual([
|
||||||
{ id: 6, label: "update", count: 1 },
|
{ id: 6, label: "update", count: 1 },
|
||||||
{ id: 7, label: "update too", 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
|
// expect 2 to be updated
|
||||||
await em.mutator(items).updateWhere(
|
await em.mutator(items).updateWhere(
|
||||||
{ count: 2 },
|
{ count: 2 },
|
||||||
{
|
{
|
||||||
count: 1
|
count: 1,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect((await em.repository(items).findMany()).data).toEqual([
|
expect((await em.repository(items).findMany()).data).toEqual([
|
||||||
{ id: 6, label: "update", count: 2 },
|
{ id: 6, label: "update", count: 2 },
|
||||||
{ id: 7, label: "update too", count: 2 },
|
{ id: 7, label: "update too", count: 2 },
|
||||||
{ id: 8, label: "keep", count: 0 }
|
{ id: 8, label: "keep", count: 0 },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -23,23 +23,23 @@ describe("Polymorphic", async () => {
|
|||||||
source: "categories",
|
source: "categories",
|
||||||
target: "media",
|
target: "media",
|
||||||
config: {
|
config: {
|
||||||
mappedBy: "image"
|
mappedBy: "image",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
// media should not see categories
|
// media should not see categories
|
||||||
expect(em.relationsOf(media.name).map((r) => r.toJSON())).toEqual([]);
|
expect(em.relationsOf(media.name).map((r) => r.toJSON())).toEqual([]);
|
||||||
|
|
||||||
// it's important that media cannot access categories
|
// it's important that media cannot access categories
|
||||||
expect(em.relations.targetRelationsOf(categories).map((r) => r.source.entity.name)).toEqual(
|
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.targetRelationsOf(media).map((r) => r.source.entity.name)).toEqual([]);
|
||||||
|
|
||||||
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.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([
|
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([]);
|
expect(em.relations.sourceRelationsOf(media).map((r) => r.target.entity.name)).toEqual([]);
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ describe("Polymorphic", async () => {
|
|||||||
"id",
|
"id",
|
||||||
"path",
|
"path",
|
||||||
"reference",
|
"reference",
|
||||||
"entity_id"
|
"entity_id",
|
||||||
]);
|
]);
|
||||||
expect(media.getSelect()).toEqual(["id", "path"]);
|
expect(media.getSelect()).toEqual(["id", "path"]);
|
||||||
});
|
});
|
||||||
@@ -60,7 +60,7 @@ describe("Polymorphic", async () => {
|
|||||||
const entities = [media, categories];
|
const entities = [media, categories];
|
||||||
const single = new PolymorphicRelation(categories, media, {
|
const single = new PolymorphicRelation(categories, media, {
|
||||||
mappedBy: "single",
|
mappedBy: "single",
|
||||||
targetCardinality: 1
|
targetCardinality: 1,
|
||||||
});
|
});
|
||||||
const multiple = new PolymorphicRelation(categories, media, { mappedBy: "multiple" });
|
const multiple = new PolymorphicRelation(categories, media, { mappedBy: "multiple" });
|
||||||
|
|
||||||
@@ -71,17 +71,17 @@ describe("Polymorphic", async () => {
|
|||||||
|
|
||||||
// it's important that media cannot access categories
|
// it's important that media cannot access categories
|
||||||
expect(em.relations.targetRelationsOf(categories).map((r) => r.source.entity.name)).toEqual(
|
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.targetRelationsOf(media).map((r) => r.source.entity.name)).toEqual([]);
|
||||||
|
|
||||||
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.entity.name)).toEqual([
|
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.entity.name)).toEqual([
|
||||||
"media",
|
"media",
|
||||||
"media"
|
"media",
|
||||||
]);
|
]);
|
||||||
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.reference)).toEqual([
|
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.reference)).toEqual([
|
||||||
"single",
|
"single",
|
||||||
"multiple"
|
"multiple",
|
||||||
]);
|
]);
|
||||||
expect(em.relations.sourceRelationsOf(media).map((r) => r.target.entity.name)).toEqual([]);
|
expect(em.relations.sourceRelationsOf(media).map((r) => r.target.entity.name)).toEqual([]);
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ describe("Polymorphic", async () => {
|
|||||||
"id",
|
"id",
|
||||||
"path",
|
"path",
|
||||||
"reference",
|
"reference",
|
||||||
"entity_id"
|
"entity_id",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
NumberField,
|
NumberField,
|
||||||
OneToOneRelation,
|
OneToOneRelation,
|
||||||
PolymorphicRelation,
|
PolymorphicRelation,
|
||||||
TextField
|
TextField,
|
||||||
} from "../../src/data";
|
} from "../../src/data";
|
||||||
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
||||||
import {
|
import {
|
||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
medium,
|
medium,
|
||||||
number,
|
number,
|
||||||
relation,
|
relation,
|
||||||
text
|
text,
|
||||||
} from "../../src/data/prototype";
|
} from "../../src/data/prototype";
|
||||||
import { MediaField } from "../../src/media/MediaField";
|
import { MediaField } from "../../src/media/MediaField";
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ describe("prototype", () => {
|
|||||||
name: text(),
|
name: text(),
|
||||||
bio: text(),
|
bio: text(),
|
||||||
age: number(),
|
age: number(),
|
||||||
some: number()
|
some: number(),
|
||||||
});
|
});
|
||||||
type db = {
|
type db = {
|
||||||
users: Schema<typeof users>;
|
users: Schema<typeof users>;
|
||||||
@@ -70,7 +70,7 @@ describe("prototype", () => {
|
|||||||
name: text({ default_value: "hello" }).required(),
|
name: text({ default_value: "hello" }).required(),
|
||||||
bio: text(),
|
bio: text(),
|
||||||
age: number(),
|
age: number(),
|
||||||
some: number().required()
|
some: number().required(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const obj: InsertSchema<typeof user> = { name: "yo", some: 1 };
|
const obj: InsertSchema<typeof user> = { name: "yo", some: 1 };
|
||||||
@@ -83,12 +83,12 @@ describe("prototype", () => {
|
|||||||
new TextField("title", { required: true }),
|
new TextField("title", { required: true }),
|
||||||
new TextField("content"),
|
new TextField("content"),
|
||||||
new DateField("created_at", {
|
new DateField("created_at", {
|
||||||
type: "datetime"
|
type: "datetime",
|
||||||
}),
|
}),
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
new MediaField("images", { entity: "posts" }),
|
new MediaField("images", { entity: "posts" }),
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
new MediaField("cover", { entity: "posts", max_items: 1 })
|
new MediaField("cover", { entity: "posts", max_items: 1 }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const posts2 = entity("posts", {
|
const posts2 = entity("posts", {
|
||||||
@@ -96,7 +96,7 @@ describe("prototype", () => {
|
|||||||
content: text(),
|
content: text(),
|
||||||
created_at: datetime(),
|
created_at: datetime(),
|
||||||
images: media(),
|
images: media(),
|
||||||
cover: medium()
|
cover: medium(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Posts = Schema<typeof posts2>;
|
type Posts = Schema<typeof posts2>;
|
||||||
@@ -117,11 +117,11 @@ describe("prototype", () => {
|
|||||||
type: "objects",
|
type: "objects",
|
||||||
values: [
|
values: [
|
||||||
{ value: "active", label: "Active" },
|
{ value: "active", label: "Active" },
|
||||||
{ value: "inactive", label: "Not active" }
|
{ value: "inactive", label: "Not active" },
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
}),
|
}),
|
||||||
new JsonField("json")
|
new JsonField("json"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const test2 = entity("test", {
|
const test2 = entity("test", {
|
||||||
@@ -134,10 +134,10 @@ describe("prototype", () => {
|
|||||||
status: enumm<"active" | "inactive">({
|
status: enumm<"active" | "inactive">({
|
||||||
enum: [
|
enum: [
|
||||||
{ value: "active", label: "Active" },
|
{ 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());
|
expect(test.toJSON()).toEqual(test2.toJSON());
|
||||||
@@ -161,12 +161,12 @@ describe("prototype", () => {
|
|||||||
// category has single image
|
// category has single image
|
||||||
new PolymorphicRelation(categories, _media, {
|
new PolymorphicRelation(categories, _media, {
|
||||||
mappedBy: "image",
|
mappedBy: "image",
|
||||||
targetCardinality: 1
|
targetCardinality: 1,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// post has multiple images
|
// post has multiple images
|
||||||
new PolymorphicRelation(posts, _media, { mappedBy: "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 = [
|
const relations2 = [
|
||||||
@@ -180,7 +180,7 @@ describe("prototype", () => {
|
|||||||
relation(categories).polyToOne(_media, { mappedBy: "image" }),
|
relation(categories).polyToOne(_media, { mappedBy: "image" }),
|
||||||
|
|
||||||
relation(posts).polyToMany(_media, { mappedBy: "images" }),
|
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()));
|
expect(relations.map((r) => r.toJSON())).toEqual(relations2.map((r) => r.toJSON()));
|
||||||
@@ -194,21 +194,21 @@ describe("prototype", () => {
|
|||||||
posts,
|
posts,
|
||||||
categories,
|
categories,
|
||||||
{
|
{
|
||||||
connectionTableMappedName: "custom"
|
connectionTableMappedName: "custom",
|
||||||
},
|
},
|
||||||
[new TextField("description")]
|
[new TextField("description")],
|
||||||
);
|
);
|
||||||
|
|
||||||
const fields = {
|
const fields = {
|
||||||
description: text()
|
description: text(),
|
||||||
};
|
};
|
||||||
let o: FieldSchema<typeof fields>;
|
let o: FieldSchema<typeof fields>;
|
||||||
const rel2 = relation(posts).manyToMany(
|
const rel2 = relation(posts).manyToMany(
|
||||||
categories,
|
categories,
|
||||||
{
|
{
|
||||||
connectionTableMappedName: "custom"
|
connectionTableMappedName: "custom",
|
||||||
},
|
},
|
||||||
fields
|
fields,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(rel.toJSON()).toEqual(rel2.toJSON());
|
expect(rel.toJSON()).toEqual(rel2.toJSON());
|
||||||
@@ -216,11 +216,11 @@ describe("prototype", () => {
|
|||||||
|
|
||||||
test("devexample", async () => {
|
test("devexample", async () => {
|
||||||
const users = entity("users", {
|
const users = entity("users", {
|
||||||
username: text()
|
username: text(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const comments = entity("comments", {
|
const comments = entity("comments", {
|
||||||
content: text()
|
content: text(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const posts = entity("posts", {
|
const posts = entity("posts", {
|
||||||
@@ -228,17 +228,17 @@ describe("prototype", () => {
|
|||||||
content: text(),
|
content: text(),
|
||||||
created_at: datetime(),
|
created_at: datetime(),
|
||||||
images: media(),
|
images: media(),
|
||||||
cover: medium()
|
cover: medium(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const categories = entity("categories", {
|
const categories = entity("categories", {
|
||||||
name: text(),
|
name: text(),
|
||||||
description: text(),
|
description: text(),
|
||||||
image: medium()
|
image: medium(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const settings = entity("settings", {
|
const settings = entity("settings", {
|
||||||
theme: text()
|
theme: text(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const test = entity("test", {
|
const test = entity("test", {
|
||||||
@@ -251,10 +251,10 @@ describe("prototype", () => {
|
|||||||
status: enumm<"active" | "inactive">({
|
status: enumm<"active" | "inactive">({
|
||||||
enum: [
|
enum: [
|
||||||
{ value: "active", label: "Active" },
|
{ 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", {});
|
const _media = entity("media", {});
|
||||||
@@ -270,7 +270,7 @@ describe("prototype", () => {
|
|||||||
relation(users).oneToOne(settings),
|
relation(users).oneToOne(settings),
|
||||||
|
|
||||||
relation(comments).manyToOne(users, { required: true }),
|
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;
|
const obj: Schema<typeof test> = {} as any;
|
||||||
@@ -281,12 +281,12 @@ describe("prototype", () => {
|
|||||||
{
|
{
|
||||||
posts: entity("posts", { name: text(), slug: text().required() }),
|
posts: entity("posts", { name: text(), slug: text().required() }),
|
||||||
comments: entity("comments", { some: text() }),
|
comments: entity("comments", { some: text() }),
|
||||||
users: entity("users", { email: text() })
|
users: entity("users", { email: text() }),
|
||||||
},
|
},
|
||||||
({ relation, index }, { posts, comments, users }) => {
|
({ relation, index }, { posts, comments, users }) => {
|
||||||
relation(posts).manyToOne(comments).manyToOne(users);
|
relation(posts).manyToOne(comments).manyToOne(users);
|
||||||
index(posts).on(["name"]).on(["slug"], true);
|
index(posts).on(["name"]).on(["slug"], true);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
type LocalDb = (typeof _em)["DB"];
|
type LocalDb = (typeof _em)["DB"];
|
||||||
@@ -294,7 +294,7 @@ describe("prototype", () => {
|
|||||||
const es = [
|
const es = [
|
||||||
new Entity("posts", [new TextField("name"), new TextField("slug", { required: true })]),
|
new Entity("posts", [new TextField("name"), new TextField("slug", { required: true })]),
|
||||||
new Entity("comments", [new TextField("some")]),
|
new Entity("comments", [new TextField("some")]),
|
||||||
new Entity("users", [new TextField("email")])
|
new Entity("users", [new TextField("email")]),
|
||||||
];
|
];
|
||||||
const _em2 = new EntityManager(
|
const _em2 = new EntityManager(
|
||||||
es,
|
es,
|
||||||
@@ -302,8 +302,8 @@ describe("prototype", () => {
|
|||||||
[new ManyToOneRelation(es[0], es[1]), new ManyToOneRelation(es[0], es[2])],
|
[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("name")!]),
|
||||||
new EntityIndex(es[0], [es[0].field("slug")!], true)
|
new EntityIndex(es[0], [es[0].field("slug")!], true),
|
||||||
]
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
ManyToOneRelation,
|
ManyToOneRelation,
|
||||||
OneToOneRelation,
|
OneToOneRelation,
|
||||||
PolymorphicRelation,
|
PolymorphicRelation,
|
||||||
RelationField
|
RelationField,
|
||||||
} from "../../src/data/relations";
|
} from "../../src/data/relations";
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
|
|
||||||
@@ -22,32 +22,32 @@ describe("Relations", async () => {
|
|||||||
const r1 = new RelationField("users_id", {
|
const r1 = new RelationField("users_id", {
|
||||||
reference: "users",
|
reference: "users",
|
||||||
target: "users",
|
target: "users",
|
||||||
target_field: "id"
|
target_field: "id",
|
||||||
});
|
});
|
||||||
|
|
||||||
const sql1 = schema
|
const sql1 = schema
|
||||||
.createTable("posts")
|
.createTable("posts")
|
||||||
.addColumn(...r1.schema()!)
|
.addColumn(...em.connection.getFieldSchema(r1.schema())!)
|
||||||
.compile().sql;
|
.compile().sql;
|
||||||
|
|
||||||
expect(sql1).toBe(
|
expect(sql1).toBe(
|
||||||
'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(new Entity("users"), "author");
|
||||||
const r2 = new RelationField("author_id", {
|
const r2 = new RelationField("author_id", {
|
||||||
reference: "author",
|
reference: "author",
|
||||||
target: "users",
|
target: "users",
|
||||||
target_field: "id"
|
target_field: "id",
|
||||||
});
|
});
|
||||||
|
|
||||||
const sql2 = schema
|
const sql2 = schema
|
||||||
.createTable("posts")
|
.createTable("posts")
|
||||||
.addColumn(...r2.schema()!)
|
.addColumn(...em.connection.getFieldSchema(r2.schema())!)
|
||||||
.compile().sql;
|
.compile().sql;
|
||||||
|
|
||||||
expect(sql2).toBe(
|
expect(sql2).toBe(
|
||||||
'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 +57,7 @@ describe("Relations", async () => {
|
|||||||
reference: "users",
|
reference: "users",
|
||||||
target: "users",
|
target: "users",
|
||||||
target_field: "id",
|
target_field: "id",
|
||||||
required: true
|
required: true,
|
||||||
});
|
});
|
||||||
expect(r1.isRequired()).toBeTrue();
|
expect(r1.isRequired()).toBeTrue();
|
||||||
});
|
});
|
||||||
@@ -66,8 +66,8 @@ describe("Relations", async () => {
|
|||||||
const users = new Entity("users", [new TextField("username")]);
|
const users = new Entity("users", [new TextField("username")]);
|
||||||
const posts = new Entity("posts", [
|
const posts = new Entity("posts", [
|
||||||
new TextField("title", {
|
new TextField("title", {
|
||||||
maxLength: 2
|
maxLength: 2,
|
||||||
})
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const entities = [users, posts];
|
const entities = [users, posts];
|
||||||
@@ -122,7 +122,7 @@ describe("Relations", async () => {
|
|||||||
.selectFrom(users.name)
|
.selectFrom(users.name)
|
||||||
.select((eb) => postAuthorRel.buildWith(users, "posts")(eb).as("posts"));
|
.select((eb) => postAuthorRel.buildWith(users, "posts")(eb).as("posts"));
|
||||||
expect(selectPostsFromUsers.compile().sql).toBe(
|
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);
|
expect(postAuthorRel!.getField()).toBeInstanceOf(RelationField);
|
||||||
const userObj = { id: 1, username: "test" };
|
const userObj = { id: 1, username: "test" };
|
||||||
@@ -142,7 +142,7 @@ describe("Relations", async () => {
|
|||||||
.select((eb) => postAuthorRel.buildWith(posts, "author")(eb).as("author"));
|
.select((eb) => postAuthorRel.buildWith(posts, "author")(eb).as("author"));
|
||||||
|
|
||||||
expect(selectUsersFromPosts.compile().sql).toBe(
|
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);
|
expect(postAuthorRel.getField()).toBeInstanceOf(RelationField);
|
||||||
const postObj = { id: 1, title: "test" };
|
const postObj = { id: 1, title: "test" };
|
||||||
@@ -158,7 +158,7 @@ describe("Relations", async () => {
|
|||||||
$detach: false,
|
$detach: false,
|
||||||
primary: undefined,
|
primary: undefined,
|
||||||
cardinality: undefined,
|
cardinality: undefined,
|
||||||
relation_type: "n:1"
|
relation_type: "n:1",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(postAuthorRel!.helper(posts.name)!.getMutationInfo()).toEqual({
|
expect(postAuthorRel!.helper(posts.name)!.getMutationInfo()).toEqual({
|
||||||
@@ -170,7 +170,7 @@ describe("Relations", async () => {
|
|||||||
$detach: false,
|
$detach: false,
|
||||||
primary: "id",
|
primary: "id",
|
||||||
cardinality: 1,
|
cardinality: 1,
|
||||||
relation_type: "n:1"
|
relation_type: "n:1",
|
||||||
});
|
});
|
||||||
|
|
||||||
/*console.log("ManyToOne (source=posts, target=users)");
|
/*console.log("ManyToOne (source=posts, target=users)");
|
||||||
@@ -225,7 +225,7 @@ describe("Relations", async () => {
|
|||||||
$detach: false,
|
$detach: false,
|
||||||
primary: "id",
|
primary: "id",
|
||||||
cardinality: 1,
|
cardinality: 1,
|
||||||
relation_type: "1:1"
|
relation_type: "1:1",
|
||||||
});
|
});
|
||||||
expect(userSettingRel!.helper(settings.name)!.getMutationInfo()).toEqual({
|
expect(userSettingRel!.helper(settings.name)!.getMutationInfo()).toEqual({
|
||||||
reference: "users",
|
reference: "users",
|
||||||
@@ -236,7 +236,7 @@ describe("Relations", async () => {
|
|||||||
$detach: false,
|
$detach: false,
|
||||||
primary: undefined,
|
primary: undefined,
|
||||||
cardinality: 1,
|
cardinality: 1,
|
||||||
relation_type: "1:1"
|
relation_type: "1:1",
|
||||||
});
|
});
|
||||||
|
|
||||||
/*console.log("");
|
/*console.log("");
|
||||||
@@ -310,16 +310,16 @@ describe("Relations", async () => {
|
|||||||
*/
|
*/
|
||||||
const selectCategoriesFromPosts = kysely
|
const selectCategoriesFromPosts = kysely
|
||||||
.selectFrom(posts.name)
|
.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(
|
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
|
const selectPostsFromCategories = kysely
|
||||||
.selectFrom(categories.name)
|
.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(
|
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
|
// mutation info
|
||||||
@@ -332,7 +332,7 @@ describe("Relations", async () => {
|
|||||||
$detach: true,
|
$detach: true,
|
||||||
primary: "id",
|
primary: "id",
|
||||||
cardinality: undefined,
|
cardinality: undefined,
|
||||||
relation_type: "m:n"
|
relation_type: "m:n",
|
||||||
});
|
});
|
||||||
expect(relations[0].helper(categories.name)!.getMutationInfo()).toEqual({
|
expect(relations[0].helper(categories.name)!.getMutationInfo()).toEqual({
|
||||||
reference: "posts",
|
reference: "posts",
|
||||||
@@ -343,7 +343,7 @@ describe("Relations", async () => {
|
|||||||
$detach: false,
|
$detach: false,
|
||||||
primary: undefined,
|
primary: undefined,
|
||||||
cardinality: undefined,
|
cardinality: undefined,
|
||||||
relation_type: "m:n"
|
relation_type: "m:n",
|
||||||
});
|
});
|
||||||
|
|
||||||
/*console.log("");
|
/*console.log("");
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ describe("[data] Entity", async () => {
|
|||||||
new TextField("name", { required: true }),
|
new TextField("name", { required: true }),
|
||||||
new TextField("description"),
|
new TextField("description"),
|
||||||
new NumberField("age", { fillable: false, default_value: 18 }),
|
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 () => {
|
test("getSelect", async () => {
|
||||||
@@ -17,7 +17,7 @@ describe("[data] Entity", async () => {
|
|||||||
expect(entity.getFillableFields().map((f) => f.name)).toEqual([
|
expect(entity.getFillableFields().map((f) => f.name)).toEqual([
|
||||||
"name",
|
"name",
|
||||||
"description",
|
"description",
|
||||||
"hidden"
|
"hidden",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ describe("[data] Entity", async () => {
|
|||||||
test("getDefaultObject", async () => {
|
test("getDefaultObject", async () => {
|
||||||
expect(entity.getDefaultObject()).toEqual({
|
expect(entity.getDefaultObject()).toEqual({
|
||||||
age: 18,
|
age: 18,
|
||||||
hidden: "secret"
|
hidden: "secret",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
EntityManager,
|
EntityManager,
|
||||||
ManyToManyRelation,
|
ManyToManyRelation,
|
||||||
ManyToOneRelation,
|
ManyToOneRelation,
|
||||||
SchemaManager
|
SchemaManager,
|
||||||
} from "../../../src/data";
|
} from "../../../src/data";
|
||||||
import { UnableToConnectException } from "../../../src/data/errors";
|
import { UnableToConnectException } from "../../../src/data/errors";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
@@ -25,7 +25,7 @@ describe("[data] EntityManager", async () => {
|
|||||||
expect(await em.ping()).toBe(true);
|
expect(await em.ping()).toBe(true);
|
||||||
expect(() => em.entity("...")).toThrow();
|
expect(() => em.entity("...")).toThrow();
|
||||||
expect(() =>
|
expect(() =>
|
||||||
em.addRelation(new ManyToOneRelation(new Entity("1"), new Entity("2")))
|
em.addRelation(new ManyToOneRelation(new Entity("1"), new Entity("2"))),
|
||||||
).toThrow();
|
).toThrow();
|
||||||
expect(em.schema()).toBeInstanceOf(SchemaManager);
|
expect(em.schema()).toBeInstanceOf(SchemaManager);
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ describe("[data] EntityManager", async () => {
|
|||||||
expect(userTargetRel.map((r) => r.other(users).entity.name)).toEqual(["posts", "comments"]);
|
expect(userTargetRel.map((r) => r.other(users).entity.name)).toEqual(["posts", "comments"]);
|
||||||
expect(postTargetRel.map((r) => r.other(posts).entity.name)).toEqual([
|
expect(postTargetRel.map((r) => r.other(posts).entity.name)).toEqual([
|
||||||
"comments",
|
"comments",
|
||||||
"categories"
|
"categories",
|
||||||
]);
|
]);
|
||||||
expect(commentTargetRel.map((r) => r.other(comments).entity.name)).toEqual([]);
|
expect(commentTargetRel.map((r) => r.other(comments).entity.name)).toEqual([]);
|
||||||
expect(categoriesTargetRel.map((r) => r.other(categories).entity.name)).toEqual(["posts"]);
|
expect(categoriesTargetRel.map((r) => r.other(categories).entity.name)).toEqual(["posts"]);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ describe("[data] JoinBuilder", async () => {
|
|||||||
const em = new EntityManager([users], dummyConnection);
|
const em = new EntityManager([users], dummyConnection);
|
||||||
|
|
||||||
expect(() =>
|
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');
|
).toThrow('Relation "posts" not found');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ describe("[data] JoinBuilder", async () => {
|
|||||||
const em = new EntityManager([users, posts], dummyConnection, relations);
|
const em = new EntityManager([users, posts], dummyConnection, relations);
|
||||||
|
|
||||||
const qb = JoinBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, [
|
const qb = JoinBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, [
|
||||||
"posts"
|
"posts",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const res = qb.compile();
|
const res = qb.compile();
|
||||||
@@ -34,7 +34,7 @@ describe("[data] JoinBuilder", async () => {
|
|||||||
);*/
|
);*/
|
||||||
|
|
||||||
const qb2 = JoinBuilder.addClause(em, em.connection.kysely.selectFrom("posts"), posts, [
|
const qb2 = JoinBuilder.addClause(em, em.connection.kysely.selectFrom("posts"), posts, [
|
||||||
"author"
|
"author",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const res2 = qb2.compile();
|
const res2 = qb2.compile();
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
OneToOneRelation,
|
OneToOneRelation,
|
||||||
type RelationField,
|
type RelationField,
|
||||||
RelationMutator,
|
RelationMutator,
|
||||||
TextField
|
TextField,
|
||||||
} from "../../../src/data";
|
} from "../../../src/data";
|
||||||
import * as proto from "../../../src/data/prototype";
|
import * as proto from "../../../src/data/prototype";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
@@ -22,7 +22,7 @@ describe("[data] Mutator (base)", async () => {
|
|||||||
new TextField("label", { required: true }),
|
new TextField("label", { required: true }),
|
||||||
new NumberField("count"),
|
new NumberField("count"),
|
||||||
new TextField("hidden", { hidden: true }),
|
new TextField("hidden", { hidden: true }),
|
||||||
new TextField("not_fillable", { fillable: false })
|
new TextField("not_fillable", { fillable: false }),
|
||||||
]);
|
]);
|
||||||
const em = new EntityManager<any>([entity], dummyConnection);
|
const em = new EntityManager<any>([entity], dummyConnection);
|
||||||
await em.schema().sync({ force: true });
|
await em.schema().sync({ force: true });
|
||||||
@@ -44,7 +44,7 @@ describe("[data] Mutator (base)", async () => {
|
|||||||
test("updateOne", async () => {
|
test("updateOne", async () => {
|
||||||
const { data } = await em.mutator(entity).insertOne(payload);
|
const { data } = await em.mutator(entity).insertOne(payload);
|
||||||
const updated = await em.mutator(entity).updateOne(data.id, {
|
const updated = await em.mutator(entity).updateOne(data.id, {
|
||||||
count: 2
|
count: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(updated.parameters).toEqual([2, data.id]);
|
expect(updated.parameters).toEqual([2, data.id]);
|
||||||
@@ -77,7 +77,7 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
|||||||
|
|
||||||
// persisting relational field should just return key value to be added
|
// persisting relational field should just return key value to be added
|
||||||
expect(
|
expect(
|
||||||
postRelMutator.persistRelationField(postRelField, "users_id", userData.data.id)
|
postRelMutator.persistRelationField(postRelField, "users_id", userData.data.id),
|
||||||
).resolves.toEqual(["users_id", userData.data.id]);
|
).resolves.toEqual(["users_id", userData.data.id]);
|
||||||
|
|
||||||
// persisting invalid value should throw
|
// persisting invalid value should throw
|
||||||
@@ -86,8 +86,8 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
|||||||
// persisting reference should ...
|
// persisting reference should ...
|
||||||
expect(
|
expect(
|
||||||
postRelMutator.persistReference(relations[0]!, "users", {
|
postRelMutator.persistReference(relations[0]!, "users", {
|
||||||
$set: { id: userData.data.id }
|
$set: { id: userData.data.id },
|
||||||
})
|
}),
|
||||||
).resolves.toEqual(["users_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
|
// @todo: add what methods are allowed to relation, like $create should not be allowed for post<>users
|
||||||
|
|
||||||
@@ -99,8 +99,8 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
|||||||
expect(
|
expect(
|
||||||
em.mutator(posts).insertOne({
|
em.mutator(posts).insertOne({
|
||||||
title: "post1",
|
title: "post1",
|
||||||
users_id: 100 // user does not exist yet
|
users_id: 100, // user does not exist yet
|
||||||
})
|
}),
|
||||||
).rejects.toThrow();
|
).rejects.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
|||||||
const em = new EntityManager([items, cats], dummyConnection, relations);
|
const em = new EntityManager([items, cats], dummyConnection, relations);
|
||||||
|
|
||||||
expect(em.mutator(items).insertOne({ label: "test" })).rejects.toThrow(
|
expect(em.mutator(items).insertOne({ label: "test" })).rejects.toThrow(
|
||||||
'Field "cats_id" is required'
|
'Field "cats_id" is required',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -119,14 +119,14 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
|||||||
const { data } = await em.mutator(users).insertOne({ username: "user1" });
|
const { data } = await em.mutator(users).insertOne({ username: "user1" });
|
||||||
const res = await em.mutator(posts).insertOne({
|
const res = await em.mutator(posts).insertOne({
|
||||||
title: "post1",
|
title: "post1",
|
||||||
users_id: data.id
|
users_id: data.id,
|
||||||
});
|
});
|
||||||
expect(res.data.users_id).toBe(data.id);
|
expect(res.data.users_id).toBe(data.id);
|
||||||
|
|
||||||
// setting "null" should be allowed
|
// setting "null" should be allowed
|
||||||
const res2 = await em.mutator(posts).insertOne({
|
const res2 = await em.mutator(posts).insertOne({
|
||||||
title: "post1",
|
title: "post1",
|
||||||
users_id: null
|
users_id: null,
|
||||||
});
|
});
|
||||||
expect(res2.data.users_id).toBe(null);
|
expect(res2.data.users_id).toBe(null);
|
||||||
});
|
});
|
||||||
@@ -135,14 +135,14 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
|||||||
const { data } = await em.mutator(users).insertOne({ username: "user1" });
|
const { data } = await em.mutator(users).insertOne({ username: "user1" });
|
||||||
const res = await em.mutator(posts).insertOne({
|
const res = await em.mutator(posts).insertOne({
|
||||||
title: "post1",
|
title: "post1",
|
||||||
users: { $set: { id: data.id } }
|
users: { $set: { id: data.id } },
|
||||||
});
|
});
|
||||||
expect(res.data.users_id).toBe(data.id);
|
expect(res.data.users_id).toBe(data.id);
|
||||||
|
|
||||||
// setting "null" should be allowed
|
// setting "null" should be allowed
|
||||||
const res2 = await em.mutator(posts).insertOne({
|
const res2 = await em.mutator(posts).insertOne({
|
||||||
title: "post1",
|
title: "post1",
|
||||||
users: { $set: { id: null } }
|
users: { $set: { id: null } },
|
||||||
});
|
});
|
||||||
expect(res2.data.users_id).toBe(null);
|
expect(res2.data.users_id).toBe(null);
|
||||||
});
|
});
|
||||||
@@ -151,8 +151,8 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
|||||||
expect(
|
expect(
|
||||||
em.mutator(posts).insertOne({
|
em.mutator(posts).insertOne({
|
||||||
title: "test",
|
title: "test",
|
||||||
users: { $create: { username: "test" } }
|
users: { $create: { username: "test" } },
|
||||||
})
|
}),
|
||||||
).rejects.toThrow();
|
).rejects.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -162,27 +162,27 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
|||||||
const res2 = await em.mutator(posts).insertOne({ title: "post1" });
|
const res2 = await em.mutator(posts).insertOne({ title: "post1" });
|
||||||
|
|
||||||
const up1 = await em.mutator(posts).updateOne(res2.data.id, {
|
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);
|
expect(up1.data.users_id).toBe(res1.data.id);
|
||||||
|
|
||||||
const up2 = await em.mutator(posts).updateOne(res2.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);
|
expect(up2.data.users_id).toBe(res1_1.data.id);
|
||||||
|
|
||||||
const up3_1 = await em.mutator(posts).updateOne(res2.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);
|
expect(up3_1.data.users_id).toBe(res1.data.id);
|
||||||
|
|
||||||
const up3_2 = await em.mutator(posts).updateOne(res2.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);
|
expect(up3_2.data.users_id).toBe(res1_1.data.id);
|
||||||
|
|
||||||
const up4 = await em.mutator(posts).updateOne(res2.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);
|
expect(up4.data.users_id).toBe(null);
|
||||||
});
|
});
|
||||||
@@ -199,8 +199,8 @@ describe("[data] Mutator (OneToOne)", async () => {
|
|||||||
expect(
|
expect(
|
||||||
em.mutator(users).insertOne({
|
em.mutator(users).insertOne({
|
||||||
username: "test",
|
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();
|
).rejects.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -210,15 +210,15 @@ describe("[data] Mutator (OneToOne)", async () => {
|
|||||||
expect(
|
expect(
|
||||||
em.mutator(users).insertOne({
|
em.mutator(users).insertOne({
|
||||||
username: "test",
|
username: "test",
|
||||||
settings: { $set: { id: data.id } }
|
settings: { $set: { id: data.id } },
|
||||||
})
|
}),
|
||||||
).rejects.toThrow();
|
).rejects.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("insertOne: using $create", async () => {
|
test("insertOne: using $create", async () => {
|
||||||
const res = await em.mutator(users).insertOne({
|
const res = await em.mutator(users).insertOne({
|
||||||
username: "test",
|
username: "test",
|
||||||
settings: { $create: { theme: "dark" } }
|
settings: { $create: { theme: "dark" } },
|
||||||
});
|
});
|
||||||
expect(res.data.settings_id).toBeDefined();
|
expect(res.data.settings_id).toBeDefined();
|
||||||
});
|
});
|
||||||
@@ -288,14 +288,17 @@ describe("[data] Mutator (Events)", async () => {
|
|||||||
|
|
||||||
test("events were fired", async () => {
|
test("events were fired", async () => {
|
||||||
const { data } = await mutator.insertOne({ label: "test" });
|
const { data } = await mutator.insertOne({ label: "test" });
|
||||||
|
await mutator.emgr.executeAsyncs();
|
||||||
expect(events.has(MutatorEvents.MutatorInsertBefore.slug)).toBeTrue();
|
expect(events.has(MutatorEvents.MutatorInsertBefore.slug)).toBeTrue();
|
||||||
expect(events.has(MutatorEvents.MutatorInsertAfter.slug)).toBeTrue();
|
expect(events.has(MutatorEvents.MutatorInsertAfter.slug)).toBeTrue();
|
||||||
|
|
||||||
await mutator.updateOne(data.id, { label: "test2" });
|
await mutator.updateOne(data.id, { label: "test2" });
|
||||||
|
await mutator.emgr.executeAsyncs();
|
||||||
expect(events.has(MutatorEvents.MutatorUpdateBefore.slug)).toBeTrue();
|
expect(events.has(MutatorEvents.MutatorUpdateBefore.slug)).toBeTrue();
|
||||||
expect(events.has(MutatorEvents.MutatorUpdateAfter.slug)).toBeTrue();
|
expect(events.has(MutatorEvents.MutatorUpdateAfter.slug)).toBeTrue();
|
||||||
|
|
||||||
await mutator.deleteOne(data.id);
|
await mutator.deleteOne(data.id);
|
||||||
|
await mutator.emgr.executeAsyncs();
|
||||||
expect(events.has(MutatorEvents.MutatorDeleteBefore.slug)).toBeTrue();
|
expect(events.has(MutatorEvents.MutatorDeleteBefore.slug)).toBeTrue();
|
||||||
expect(events.has(MutatorEvents.MutatorDeleteAfter.slug)).toBeTrue();
|
expect(events.has(MutatorEvents.MutatorDeleteAfter.slug)).toBeTrue();
|
||||||
});
|
});
|
||||||
@@ -303,7 +306,7 @@ describe("[data] Mutator (Events)", async () => {
|
|||||||
test("insertOne event return is respected", async () => {
|
test("insertOne event return is respected", async () => {
|
||||||
const posts = proto.entity("posts", {
|
const posts = proto.entity("posts", {
|
||||||
title: proto.text(),
|
title: proto.text(),
|
||||||
views: proto.number()
|
views: proto.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const conn = getDummyConnection();
|
const conn = getDummyConnection();
|
||||||
@@ -318,10 +321,10 @@ describe("[data] Mutator (Events)", async () => {
|
|||||||
async (event) => {
|
async (event) => {
|
||||||
return {
|
return {
|
||||||
...event.params.data,
|
...event.params.data,
|
||||||
views: 2
|
views: 2,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
"sync"
|
"sync",
|
||||||
);
|
);
|
||||||
|
|
||||||
const mutator = em.mutator("posts");
|
const mutator = em.mutator("posts");
|
||||||
@@ -329,14 +332,14 @@ describe("[data] Mutator (Events)", async () => {
|
|||||||
expect(result.data).toEqual({
|
expect(result.data).toEqual({
|
||||||
id: 1,
|
id: 1,
|
||||||
title: "test",
|
title: "test",
|
||||||
views: 2
|
views: 2,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("updateOne event return is respected", async () => {
|
test("updateOne event return is respected", async () => {
|
||||||
const posts = proto.entity("posts", {
|
const posts = proto.entity("posts", {
|
||||||
title: proto.text(),
|
title: proto.text(),
|
||||||
views: proto.number()
|
views: proto.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const conn = getDummyConnection();
|
const conn = getDummyConnection();
|
||||||
@@ -351,10 +354,10 @@ describe("[data] Mutator (Events)", async () => {
|
|||||||
async (event) => {
|
async (event) => {
|
||||||
return {
|
return {
|
||||||
...event.params.data,
|
...event.params.data,
|
||||||
views: event.params.data.views + 1
|
views: event.params.data.views + 1,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
"sync"
|
"sync",
|
||||||
);
|
);
|
||||||
|
|
||||||
const mutator = em.mutator("posts");
|
const mutator = em.mutator("posts");
|
||||||
@@ -363,7 +366,7 @@ describe("[data] Mutator (Events)", async () => {
|
|||||||
expect(result.data).toEqual({
|
expect(result.data).toEqual({
|
||||||
id: 1,
|
id: 1,
|
||||||
title: "test",
|
title: "test",
|
||||||
views: 3
|
views: 3,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import type { Kysely, Transaction } from "kysely";
|
import type { Kysely, Transaction } from "kysely";
|
||||||
import { Perf } from "../../../src/core/utils";
|
import { Perf } from "core/utils";
|
||||||
import {
|
import {
|
||||||
Entity,
|
Entity,
|
||||||
EntityManager,
|
EntityManager,
|
||||||
LibsqlConnection,
|
LibsqlConnection,
|
||||||
ManyToOneRelation,
|
ManyToOneRelation,
|
||||||
RepositoryEvents,
|
RepositoryEvents,
|
||||||
TextField
|
TextField,
|
||||||
} from "../../../src/data";
|
entity as $entity,
|
||||||
|
text as $text,
|
||||||
|
em as $em,
|
||||||
|
} from "data";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
|
|
||||||
type E = Kysely<any> | Transaction<any>;
|
type E = Kysely<any> | Transaction<any>;
|
||||||
@@ -70,13 +73,13 @@ describe("[Repository]", async () => {
|
|||||||
const q1 = selectQ(conn).compile();
|
const q1 = selectQ(conn).compile();
|
||||||
const res = await client.execute({
|
const res = await client.execute({
|
||||||
sql: q1.sql,
|
sql: q1.sql,
|
||||||
args: q1.parameters as any
|
args: q1.parameters as any,
|
||||||
});
|
});
|
||||||
|
|
||||||
const q2 = countQ(conn).compile();
|
const q2 = countQ(conn).compile();
|
||||||
const count = await client.execute({
|
const count = await client.execute({
|
||||||
sql: q2.sql,
|
sql: q2.sql,
|
||||||
args: q2.parameters as any
|
args: q2.parameters as any,
|
||||||
});
|
});
|
||||||
return [res, count];
|
return [res, count];
|
||||||
}
|
}
|
||||||
@@ -93,7 +96,7 @@ describe("[Repository]", async () => {
|
|||||||
const exec = async (
|
const exec = async (
|
||||||
name: string,
|
name: string,
|
||||||
fn: (em: EntityManager<any>) => Promise<any>,
|
fn: (em: EntityManager<any>) => Promise<any>,
|
||||||
em: EntityManager<any>
|
em: EntityManager<any>,
|
||||||
) => {
|
) => {
|
||||||
const res = await Perf.execute(() => fn(em), times);
|
const res = await Perf.execute(() => fn(em), times);
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
@@ -102,7 +105,7 @@ describe("[Repository]", async () => {
|
|||||||
total: res.total.toFixed(2),
|
total: res.total.toFixed(2),
|
||||||
avg: (res.total / times).toFixed(2),
|
avg: (res.total / times).toFixed(2),
|
||||||
first: res.marks[0].time.toFixed(2),
|
first: res.marks[0].time.toFixed(2),
|
||||||
last: res.marks[res.marks.length - 1].time.toFixed(2)
|
last: res.marks[res.marks.length - 1].time.toFixed(2),
|
||||||
};
|
};
|
||||||
console.log(info.name, info, res.marks);
|
console.log(info.name, info, res.marks);
|
||||||
return info;
|
return info;
|
||||||
@@ -177,13 +180,54 @@ describe("[Repository]", async () => {
|
|||||||
const res5 = await em.repository(items).exists({});
|
const res5 = await em.repository(items).exists({});
|
||||||
expect(res5.exists).toBe(true);
|
expect(res5.exists).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("option: silent", async () => {
|
||||||
|
const em = $em({
|
||||||
|
items: $entity("items", {
|
||||||
|
label: $text(),
|
||||||
|
}),
|
||||||
|
}).proto.withConnection(getDummyConnection().dummyConnection);
|
||||||
|
|
||||||
|
// should throw because table doesn't exist
|
||||||
|
expect(em.repo("items").findMany({})).rejects.toThrow(/no such table/);
|
||||||
|
// should silently return empty result
|
||||||
|
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")
|
||||||
|
.findMany({})
|
||||||
|
.then((r) => [r.meta.count, r.meta.total]),
|
||||||
|
).resolves.toEqual([0, 0]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
em
|
||||||
|
.repo("items", { includeCounts: false })
|
||||||
|
.findMany({})
|
||||||
|
.then((r) => [r.meta.count, r.meta.total]),
|
||||||
|
).resolves.toEqual([undefined, undefined]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("[data] Repository (Events)", async () => {
|
describe("[data] Repository (Events)", async () => {
|
||||||
const items = new Entity("items", [new TextField("label")]);
|
const items = new Entity("items", [new TextField("label")]);
|
||||||
const categories = new Entity("categories", [new TextField("label")]);
|
const categories = new Entity("categories", [new TextField("label")]);
|
||||||
const em = new EntityManager([items, categories], dummyConnection, [
|
const em = new EntityManager([items, categories], dummyConnection, [
|
||||||
new ManyToOneRelation(categories, items)
|
new ManyToOneRelation(categories, items),
|
||||||
]);
|
]);
|
||||||
await em.schema().sync({ force: true });
|
await em.schema().sync({ force: true });
|
||||||
const events = new Map<string, any>();
|
const events = new Map<string, any>();
|
||||||
@@ -198,22 +242,27 @@ describe("[data] Repository (Events)", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("events were fired", async () => {
|
test("events were fired", async () => {
|
||||||
await em.repository(items).findId(1);
|
const repo = em.repository(items);
|
||||||
|
await repo.findId(1);
|
||||||
|
await repo.emgr.executeAsyncs();
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue();
|
expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue();
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
|
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
|
||||||
events.clear();
|
events.clear();
|
||||||
|
|
||||||
await em.repository(items).findOne({ id: 1 });
|
await repo.findOne({ id: 1 });
|
||||||
|
await repo.emgr.executeAsyncs();
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue();
|
expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue();
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
|
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
|
||||||
events.clear();
|
events.clear();
|
||||||
|
|
||||||
await em.repository(items).findMany({ where: { id: 1 } });
|
await repo.findMany({ where: { id: 1 } });
|
||||||
|
await repo.emgr.executeAsyncs();
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindManyBefore.slug)).toBeTrue();
|
expect(events.has(RepositoryEvents.RepositoryFindManyBefore.slug)).toBeTrue();
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue();
|
expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue();
|
||||||
events.clear();
|
events.clear();
|
||||||
|
|
||||||
await em.repository(items).findManyByReference(1, "categories");
|
await repo.findManyByReference(1, "categories");
|
||||||
|
await repo.emgr.executeAsyncs();
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindManyBefore.slug)).toBeTrue();
|
expect(events.has(RepositoryEvents.RepositoryFindManyBefore.slug)).toBeTrue();
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue();
|
expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue();
|
||||||
events.clear();
|
events.clear();
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ describe("SchemaManager tests", async () => {
|
|||||||
const em = new EntityManager([entity], dummyConnection, [], [index]);
|
const em = new EntityManager([entity], dummyConnection, [], [index]);
|
||||||
const schema = new SchemaManager(em);
|
const schema = new SchemaManager(em);
|
||||||
|
|
||||||
const introspection = schema.getIntrospectionFromEntity(em.entities[0]);
|
const introspection = schema.getIntrospectionFromEntity(em.entities[0]!);
|
||||||
expect(introspection).toEqual({
|
expect(introspection).toEqual({
|
||||||
name: "test",
|
name: "test",
|
||||||
isView: false,
|
isView: false,
|
||||||
@@ -26,7 +26,7 @@ describe("SchemaManager tests", async () => {
|
|||||||
isNullable: true,
|
isNullable: true,
|
||||||
isAutoIncrementing: true,
|
isAutoIncrementing: true,
|
||||||
hasDefaultValue: false,
|
hasDefaultValue: false,
|
||||||
comment: undefined
|
comment: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "username",
|
name: "username",
|
||||||
@@ -34,7 +34,7 @@ describe("SchemaManager tests", async () => {
|
|||||||
isNullable: true,
|
isNullable: true,
|
||||||
isAutoIncrementing: false,
|
isAutoIncrementing: false,
|
||||||
hasDefaultValue: false,
|
hasDefaultValue: false,
|
||||||
comment: undefined
|
comment: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "email",
|
name: "email",
|
||||||
@@ -42,7 +42,7 @@ describe("SchemaManager tests", async () => {
|
|||||||
isNullable: true,
|
isNullable: true,
|
||||||
isAutoIncrementing: false,
|
isAutoIncrementing: false,
|
||||||
hasDefaultValue: false,
|
hasDefaultValue: false,
|
||||||
comment: undefined
|
comment: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "bio",
|
name: "bio",
|
||||||
@@ -50,8 +50,8 @@ describe("SchemaManager tests", async () => {
|
|||||||
isNullable: true,
|
isNullable: true,
|
||||||
isAutoIncrementing: false,
|
isAutoIncrementing: false,
|
||||||
hasDefaultValue: false,
|
hasDefaultValue: false,
|
||||||
comment: undefined
|
comment: undefined,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
indices: [
|
indices: [
|
||||||
{
|
{
|
||||||
@@ -61,11 +61,11 @@ describe("SchemaManager tests", async () => {
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
name: "email",
|
name: "email",
|
||||||
order: 0
|
order: 0,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -77,10 +77,10 @@ describe("SchemaManager tests", async () => {
|
|||||||
new Entity(table, [
|
new Entity(table, [
|
||||||
new TextField("username"),
|
new TextField("username"),
|
||||||
new TextField("email"),
|
new TextField("email"),
|
||||||
new TextField("bio")
|
new TextField("bio"),
|
||||||
])
|
]),
|
||||||
],
|
],
|
||||||
dummyConnection
|
dummyConnection,
|
||||||
);
|
);
|
||||||
const kysely = em.connection.kysely;
|
const kysely = em.connection.kysely;
|
||||||
|
|
||||||
@@ -101,15 +101,15 @@ describe("SchemaManager tests", async () => {
|
|||||||
name: table,
|
name: table,
|
||||||
isNew: false,
|
isNew: false,
|
||||||
columns: { add: ["bio"], drop: [], change: [] },
|
columns: { add: ["bio"], drop: [], change: [] },
|
||||||
indices: { add: [], drop: [index] }
|
indices: { add: [], drop: [index] },
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// now sync
|
// now sync
|
||||||
await schema.sync({ force: true, drop: true });
|
await schema.sync({ force: true, drop: true });
|
||||||
const diffAfter = await schema.getDiff();
|
const diffAfter = await schema.getDiff();
|
||||||
|
|
||||||
console.log("diffAfter", diffAfter);
|
//console.log("diffAfter", diffAfter);
|
||||||
expect(diffAfter.length).toBe(0);
|
expect(diffAfter.length).toBe(0);
|
||||||
|
|
||||||
await kysely.schema.dropTable(table).execute();
|
await kysely.schema.dropTable(table).execute();
|
||||||
@@ -119,7 +119,7 @@ describe("SchemaManager tests", async () => {
|
|||||||
const table = "drop_column";
|
const table = "drop_column";
|
||||||
const em = new EntityManager(
|
const em = new EntityManager(
|
||||||
[new Entity(table, [new TextField("username")])],
|
[new Entity(table, [new TextField("username")])],
|
||||||
dummyConnection
|
dummyConnection,
|
||||||
);
|
);
|
||||||
const kysely = em.connection.kysely;
|
const kysely = em.connection.kysely;
|
||||||
|
|
||||||
@@ -141,10 +141,10 @@ describe("SchemaManager tests", async () => {
|
|||||||
columns: {
|
columns: {
|
||||||
add: [],
|
add: [],
|
||||||
drop: ["email"],
|
drop: ["email"],
|
||||||
change: []
|
change: [],
|
||||||
|
},
|
||||||
|
indices: { add: [], drop: [] },
|
||||||
},
|
},
|
||||||
indices: { add: [], drop: [] }
|
|
||||||
}
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// now sync
|
// now sync
|
||||||
@@ -165,15 +165,15 @@ describe("SchemaManager tests", async () => {
|
|||||||
new Entity(usersTable, [
|
new Entity(usersTable, [
|
||||||
new TextField("username"),
|
new TextField("username"),
|
||||||
new TextField("email"),
|
new TextField("email"),
|
||||||
new TextField("bio")
|
new TextField("bio"),
|
||||||
]),
|
]),
|
||||||
new Entity(postsTable, [
|
new Entity(postsTable, [
|
||||||
new TextField("title"),
|
new TextField("title"),
|
||||||
new TextField("content"),
|
new TextField("content"),
|
||||||
new TextField("created_at")
|
new TextField("created_at"),
|
||||||
])
|
]),
|
||||||
],
|
],
|
||||||
dummyConnection
|
dummyConnection,
|
||||||
);
|
);
|
||||||
const kysely = em.connection.kysely;
|
const kysely = em.connection.kysely;
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@ describe("SchemaManager tests", async () => {
|
|||||||
name: usersTable,
|
name: usersTable,
|
||||||
isNew: false,
|
isNew: false,
|
||||||
columns: { add: ["bio"], drop: [], change: [] },
|
columns: { add: ["bio"], drop: [], change: [] },
|
||||||
indices: { add: [], drop: [] }
|
indices: { add: [], drop: [] },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: postsTable,
|
name: postsTable,
|
||||||
@@ -200,10 +200,10 @@ describe("SchemaManager tests", async () => {
|
|||||||
columns: {
|
columns: {
|
||||||
add: ["id", "title", "content", "created_at"],
|
add: ["id", "title", "content", "created_at"],
|
||||||
drop: [],
|
drop: [],
|
||||||
change: []
|
change: [],
|
||||||
|
},
|
||||||
|
indices: { add: [], drop: [] },
|
||||||
},
|
},
|
||||||
indices: { add: [], drop: [] }
|
|
||||||
}
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// now sync
|
// now sync
|
||||||
@@ -228,8 +228,8 @@ describe("SchemaManager tests", async () => {
|
|||||||
name: entity.name,
|
name: entity.name,
|
||||||
isNew: true,
|
isNew: true,
|
||||||
columns: { add: ["id", "email"], drop: [], change: [] },
|
columns: { add: ["id", "email"], drop: [], change: [] },
|
||||||
indices: { add: [index.name!], drop: [] }
|
indices: { add: [index.name!], drop: [] },
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// sync and then check again
|
// sync and then check again
|
||||||
@@ -256,8 +256,8 @@ describe("SchemaManager tests", async () => {
|
|||||||
name: entity.name,
|
name: entity.name,
|
||||||
isNew: false,
|
isNew: false,
|
||||||
columns: { add: [], drop: [], change: [] },
|
columns: { add: [], drop: [], change: [] },
|
||||||
indices: { add: [index.name!], drop: [] }
|
indices: { add: [index.name!], drop: [] },
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// sync and then check again
|
// sync and then check again
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { _jsonp } from "../../../src/core/utils";
|
|
||||||
import {
|
import {
|
||||||
Entity,
|
Entity,
|
||||||
EntityManager,
|
EntityManager,
|
||||||
@@ -7,10 +6,10 @@ import {
|
|||||||
ManyToOneRelation,
|
ManyToOneRelation,
|
||||||
PolymorphicRelation,
|
PolymorphicRelation,
|
||||||
TextField,
|
TextField,
|
||||||
WithBuilder
|
WithBuilder,
|
||||||
} from "../../../src/data";
|
} from "../../../src/data";
|
||||||
import * as proto from "../../../src/data/prototype";
|
import * as proto from "../../../src/data/prototype";
|
||||||
import { compileQb, prettyPrintQb, schemaToEm } from "../../helper";
|
import { schemaToEm } from "../../helper";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
|
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
@@ -21,32 +20,32 @@ describe("[data] WithBuilder", async () => {
|
|||||||
{
|
{
|
||||||
posts: proto.entity("posts", {}),
|
posts: proto.entity("posts", {}),
|
||||||
users: proto.entity("users", {}),
|
users: proto.entity("users", {}),
|
||||||
media: proto.entity("media", {})
|
media: proto.entity("media", {}),
|
||||||
},
|
},
|
||||||
({ relation }, { posts, users, media }) => {
|
({ relation }, { posts, users, media }) => {
|
||||||
relation(posts).manyToOne(users);
|
relation(posts).manyToOne(users);
|
||||||
relation(users).polyToOne(media, { mappedBy: "avatar" });
|
relation(users).polyToOne(media, { mappedBy: "avatar" });
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
const em = schemaToEm(schema);
|
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", {})).toBe(0);
|
||||||
expect(WithBuilder.validateWiths(em, "posts", { users: {} })).toBe(1);
|
expect(WithBuilder.validateWiths(em, "posts", { users: {} })).toBe(1);
|
||||||
expect(
|
expect(
|
||||||
WithBuilder.validateWiths(em, "posts", {
|
WithBuilder.validateWiths(em, "posts", {
|
||||||
users: {
|
users: {
|
||||||
with: { avatar: {} }
|
with: { avatar: {} },
|
||||||
}
|
},
|
||||||
})
|
}),
|
||||||
).toBe(2);
|
).toBe(2);
|
||||||
expect(() => WithBuilder.validateWiths(em, "posts", { author: {} })).toThrow();
|
expect(() => WithBuilder.validateWiths(em, "posts", { author: {} })).toThrow();
|
||||||
expect(() =>
|
expect(() =>
|
||||||
WithBuilder.validateWiths(em, "posts", {
|
WithBuilder.validateWiths(em, "posts", {
|
||||||
users: {
|
users: {
|
||||||
with: { glibberish: {} }
|
with: { glibberish: {} },
|
||||||
}
|
},
|
||||||
})
|
}),
|
||||||
).toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -56,8 +55,8 @@ describe("[data] WithBuilder", async () => {
|
|||||||
|
|
||||||
expect(() =>
|
expect(() =>
|
||||||
WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, {
|
WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, {
|
||||||
posts: {}
|
posts: {},
|
||||||
})
|
}),
|
||||||
).toThrow('Relation "users<>posts" not found');
|
).toThrow('Relation "users<>posts" not found');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -68,13 +67,13 @@ describe("[data] WithBuilder", async () => {
|
|||||||
const em = new EntityManager([users, posts], dummyConnection, relations);
|
const em = new EntityManager([users, posts], dummyConnection, relations);
|
||||||
|
|
||||||
const qb = WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, {
|
const qb = WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, {
|
||||||
posts: {}
|
posts: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = qb.compile();
|
const res = qb.compile();
|
||||||
|
|
||||||
expect(res.sql).toBe(
|
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]);
|
expect(res.parameters).toEqual([10, 0]);
|
||||||
|
|
||||||
@@ -83,14 +82,14 @@ describe("[data] WithBuilder", async () => {
|
|||||||
em.connection.kysely.selectFrom("posts"),
|
em.connection.kysely.selectFrom("posts"),
|
||||||
posts, // @todo: try with "users", it gives output!
|
posts, // @todo: try with "users", it gives output!
|
||||||
{
|
{
|
||||||
author: {}
|
author: {},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const res2 = qb2.compile();
|
const res2 = qb2.compile();
|
||||||
|
|
||||||
expect(res2.sql).toBe(
|
expect(res2.sql).toBe(
|
||||||
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "users"."id" as "id", "users"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" order by "users"."id" asc limit ? offset ?) as obj) as "author" from "posts"'
|
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "users"."id" as "id", "users"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" order by "users"."id" asc limit ? offset ?) as obj) as "author" from "posts"',
|
||||||
);
|
);
|
||||||
expect(res2.parameters).toEqual([1, 0]);
|
expect(res2.parameters).toEqual([1, 0]);
|
||||||
});
|
});
|
||||||
@@ -124,7 +123,7 @@ describe("[data] WithBuilder", async () => {
|
|||||||
.values([
|
.values([
|
||||||
{ posts_id: 1, categories_id: 1 },
|
{ posts_id: 1, categories_id: 1 },
|
||||||
{ posts_id: 2, categories_id: 2 },
|
{ posts_id: 2, categories_id: 2 },
|
||||||
{ posts_id: 1, categories_id: 2 }
|
{ posts_id: 1, categories_id: 2 },
|
||||||
])
|
])
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
@@ -138,14 +137,14 @@ describe("[data] WithBuilder", async () => {
|
|||||||
title: "fashion post",
|
title: "fashion post",
|
||||||
categories: [
|
categories: [
|
||||||
{ id: 1, label: "fashion" },
|
{ id: 1, label: "fashion" },
|
||||||
{ id: 2, label: "beauty" }
|
{ id: 2, label: "beauty" },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: "beauty post",
|
title: "beauty post",
|
||||||
categories: [{ id: 2, label: "beauty" }]
|
categories: [{ id: 2, label: "beauty" }],
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const res2 = await em.repository(categories).findMany({ with: { posts: {} } });
|
const res2 = await em.repository(categories).findMany({ with: { posts: {} } });
|
||||||
@@ -156,21 +155,21 @@ describe("[data] WithBuilder", async () => {
|
|||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
label: "fashion",
|
label: "fashion",
|
||||||
posts: [{ id: 1, title: "fashion post" }]
|
posts: [{ id: 1, title: "fashion post" }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
label: "beauty",
|
label: "beauty",
|
||||||
posts: [
|
posts: [
|
||||||
{ id: 1, title: "fashion post" },
|
{ id: 1, title: "fashion post" },
|
||||||
{ id: 2, title: "beauty post" }
|
{ id: 2, title: "beauty post" },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
label: "tech",
|
label: "tech",
|
||||||
posts: []
|
posts: [],
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -181,7 +180,7 @@ describe("[data] WithBuilder", async () => {
|
|||||||
const entities = [media, categories];
|
const entities = [media, categories];
|
||||||
const single = new PolymorphicRelation(categories, media, {
|
const single = new PolymorphicRelation(categories, media, {
|
||||||
mappedBy: "single",
|
mappedBy: "single",
|
||||||
targetCardinality: 1
|
targetCardinality: 1,
|
||||||
});
|
});
|
||||||
const multiple = new PolymorphicRelation(categories, media, { mappedBy: "multiple" });
|
const multiple = new PolymorphicRelation(categories, media, { mappedBy: "multiple" });
|
||||||
|
|
||||||
@@ -191,11 +190,11 @@ describe("[data] WithBuilder", async () => {
|
|||||||
em,
|
em,
|
||||||
em.connection.kysely.selectFrom("categories"),
|
em.connection.kysely.selectFrom("categories"),
|
||||||
categories,
|
categories,
|
||||||
{ single: {} }
|
{ single: {} },
|
||||||
);
|
);
|
||||||
const res = qb.compile();
|
const res = qb.compile();
|
||||||
expect(res.sql).toBe(
|
expect(res.sql).toBe(
|
||||||
'select (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as obj) as "single" from "categories"'
|
'select (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as obj) as "single" from "categories"',
|
||||||
);
|
);
|
||||||
expect(res.parameters).toEqual(["categories.single", 1, 0]);
|
expect(res.parameters).toEqual(["categories.single", 1, 0]);
|
||||||
|
|
||||||
@@ -203,11 +202,11 @@ describe("[data] WithBuilder", async () => {
|
|||||||
em,
|
em,
|
||||||
em.connection.kysely.selectFrom("categories"),
|
em.connection.kysely.selectFrom("categories"),
|
||||||
categories,
|
categories,
|
||||||
{ multiple: {} }
|
{ multiple: {} },
|
||||||
);
|
);
|
||||||
const res2 = qb2.compile();
|
const res2 = qb2.compile();
|
||||||
expect(res2.sql).toBe(
|
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]);
|
expect(res2.parameters).toEqual(["categories.multiple", 10, 0]);
|
||||||
});
|
});
|
||||||
@@ -240,16 +239,16 @@ describe("[data] WithBuilder", async () => {
|
|||||||
{
|
{
|
||||||
posts: proto.entity("posts", {}),
|
posts: proto.entity("posts", {}),
|
||||||
users: proto.entity("users", {
|
users: proto.entity("users", {
|
||||||
username: proto.text()
|
username: proto.text(),
|
||||||
}),
|
}),
|
||||||
media: proto.entity("media", {
|
media: proto.entity("media", {
|
||||||
path: proto.text()
|
path: proto.text(),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
({ relation }, { posts, users, media }) => {
|
({ relation }, { posts, users, media }) => {
|
||||||
relation(posts).manyToOne(users);
|
relation(posts).manyToOne(users);
|
||||||
relation(users).polyToOne(media, { mappedBy: "avatar" });
|
relation(users).polyToOne(media, { mappedBy: "avatar" });
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
const em = schemaToEm(schema);
|
const em = schemaToEm(schema);
|
||||||
|
|
||||||
@@ -265,16 +264,16 @@ describe("[data] WithBuilder", async () => {
|
|||||||
with: {
|
with: {
|
||||||
avatar: {
|
avatar: {
|
||||||
select: ["id", "path"],
|
select: ["id", "path"],
|
||||||
limit: 2 // ignored
|
limit: 2, // ignored
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
//prettyPrintQb(qb);
|
//prettyPrintQb(qb);
|
||||||
expect(qb.compile().sql).toBe(
|
expect(qb.compile().sql).toBe(
|
||||||
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username", \'avatar\', "obj"."avatar") from (select "users"."id" as "id", "users"."username" as "username", (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "users"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as obj) as "avatar" from "users" as "users" where "users"."id" = "posts"."users_id" order by "users"."username" asc limit ? offset ?) as obj) as "users" from "posts"'
|
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username", \'avatar\', "obj"."avatar") from (select "users"."id" as "id", "users"."username" as "username", (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "users"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as obj) as "avatar" from "users" as "users" where "users"."id" = "posts"."users_id" order by "users"."username" asc limit ? offset ?) as obj) as "users" from "posts"',
|
||||||
);
|
);
|
||||||
expect(qb.compile().parameters).toEqual(["users.avatar", 1, 0, 1, 0]);
|
expect(qb.compile().parameters).toEqual(["users.avatar", 1, 0, 1, 0]);
|
||||||
});
|
});
|
||||||
@@ -285,17 +284,17 @@ describe("[data] WithBuilder", async () => {
|
|||||||
posts: proto.entity("posts", {}),
|
posts: proto.entity("posts", {}),
|
||||||
comments: proto.entity("comments", {}),
|
comments: proto.entity("comments", {}),
|
||||||
users: proto.entity("users", {
|
users: proto.entity("users", {
|
||||||
username: proto.text()
|
username: proto.text(),
|
||||||
}),
|
}),
|
||||||
media: proto.entity("media", {
|
media: proto.entity("media", {
|
||||||
path: proto.text()
|
path: proto.text(),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
({ relation }, { posts, comments, users, media }) => {
|
({ relation }, { posts, comments, users, media }) => {
|
||||||
relation(posts).manyToOne(users).polyToOne(media, { mappedBy: "images" });
|
relation(posts).manyToOne(users).polyToOne(media, { mappedBy: "images" });
|
||||||
relation(users).polyToOne(media, { mappedBy: "avatar" });
|
relation(users).polyToOne(media, { mappedBy: "avatar" });
|
||||||
relation(comments).manyToOne(posts).manyToOne(users);
|
relation(comments).manyToOne(posts).manyToOne(users);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
const em = schemaToEm(schema);
|
const em = schemaToEm(schema);
|
||||||
|
|
||||||
@@ -308,15 +307,15 @@ describe("[data] WithBuilder", async () => {
|
|||||||
limit: 12,
|
limit: 12,
|
||||||
with: {
|
with: {
|
||||||
users: {
|
users: {
|
||||||
select: ["username"]
|
select: ["username"],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(qb.compile().sql).toBe(
|
expect(qb.compile().sql).toBe(
|
||||||
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'posts_id\', "agg"."posts_id", \'users_id\', "agg"."users_id", \'users\', "agg"."users")), \'[]\') from (select "comments"."id" as "id", "comments"."posts_id" as "posts_id", "comments"."users_id" as "users_id", (select json_object(\'username\', "obj"."username") from (select "users"."username" as "username" from "users" as "users" where "users"."id" = "comments"."users_id" order by "users"."id" asc limit ? offset ?) as obj) as "users" from "comments" as "comments" where "comments"."posts_id" = "posts"."id" order by "comments"."id" asc limit ? offset ?) as agg) as "comments" from "posts"'
|
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'posts_id\', "agg"."posts_id", \'users_id\', "agg"."users_id", \'users\', "agg"."users")), \'[]\') from (select "comments"."id" as "id", "comments"."posts_id" as "posts_id", "comments"."users_id" as "users_id", (select json_object(\'username\', "obj"."username") from (select "users"."username" as "username" from "users" as "users" where "users"."id" = "comments"."users_id" order by "users"."id" asc limit ? offset ?) as obj) as "users" from "comments" as "comments" where "comments"."posts_id" = "posts"."id" order by "comments"."id" asc limit ? offset ?) as agg) as "comments" from "posts"',
|
||||||
);
|
);
|
||||||
expect(qb.compile().parameters).toEqual([1, 0, 12, 0]);
|
expect(qb.compile().parameters).toEqual([1, 0, 12, 0]);
|
||||||
});
|
});
|
||||||
@@ -325,23 +324,23 @@ describe("[data] WithBuilder", async () => {
|
|||||||
const schema = proto.em(
|
const schema = proto.em(
|
||||||
{
|
{
|
||||||
posts: proto.entity("posts", {
|
posts: proto.entity("posts", {
|
||||||
title: proto.text()
|
title: proto.text(),
|
||||||
}),
|
}),
|
||||||
comments: proto.entity("comments", {
|
comments: proto.entity("comments", {
|
||||||
content: proto.text()
|
content: proto.text(),
|
||||||
}),
|
}),
|
||||||
users: proto.entity("users", {
|
users: proto.entity("users", {
|
||||||
username: proto.text()
|
username: proto.text(),
|
||||||
}),
|
}),
|
||||||
media: proto.entity("media", {
|
media: proto.entity("media", {
|
||||||
path: proto.text()
|
path: proto.text(),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
({ relation }, { posts, comments, users, media }) => {
|
({ relation }, { posts, comments, users, media }) => {
|
||||||
relation(posts).manyToOne(users).polyToOne(media, { mappedBy: "images" });
|
relation(posts).manyToOne(users).polyToOne(media, { mappedBy: "images" });
|
||||||
relation(users).polyToOne(media, { mappedBy: "avatar" });
|
relation(users).polyToOne(media, { mappedBy: "avatar" });
|
||||||
relation(comments).manyToOne(posts).manyToOne(users);
|
relation(comments).manyToOne(posts).manyToOne(users);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
const em = schemaToEm(schema);
|
const em = schemaToEm(schema);
|
||||||
await em.schema().sync({ force: true });
|
await em.schema().sync({ force: true });
|
||||||
@@ -351,7 +350,7 @@ describe("[data] WithBuilder", async () => {
|
|||||||
await em.mutator("posts").insertMany([
|
await em.mutator("posts").insertMany([
|
||||||
{ title: "post1", users_id: 1 },
|
{ title: "post1", users_id: 1 },
|
||||||
{ title: "post2", users_id: 1 },
|
{ title: "post2", users_id: 1 },
|
||||||
{ title: "post3", users_id: 2 }
|
{ title: "post3", users_id: 2 },
|
||||||
]);
|
]);
|
||||||
await em.mutator("comments").insertMany([
|
await em.mutator("comments").insertMany([
|
||||||
{ content: "comment1", posts_id: 1, users_id: 1 },
|
{ content: "comment1", posts_id: 1, users_id: 1 },
|
||||||
@@ -360,7 +359,7 @@ describe("[data] WithBuilder", async () => {
|
|||||||
{ content: "comment3", posts_id: 2, users_id: 1 },
|
{ content: "comment3", posts_id: 2, users_id: 1 },
|
||||||
{ content: "comment4", posts_id: 2, users_id: 2 },
|
{ content: "comment4", posts_id: 2, users_id: 2 },
|
||||||
{ content: "comment5", posts_id: 3, users_id: 1 },
|
{ 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({
|
const result = await em.repo("posts").findMany({
|
||||||
@@ -371,11 +370,11 @@ describe("[data] WithBuilder", async () => {
|
|||||||
select: ["content"],
|
select: ["content"],
|
||||||
with: {
|
with: {
|
||||||
users: {
|
users: {
|
||||||
select: ["username"]
|
select: ["username"],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.data).toEqual([
|
expect(result.data).toEqual([
|
||||||
@@ -385,16 +384,16 @@ describe("[data] WithBuilder", async () => {
|
|||||||
{
|
{
|
||||||
content: "comment1",
|
content: "comment1",
|
||||||
users: {
|
users: {
|
||||||
username: "user1"
|
username: "user1",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
content: "comment1-1",
|
content: "comment1-1",
|
||||||
users: {
|
users: {
|
||||||
username: "user1"
|
username: "user1",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "post2",
|
title: "post2",
|
||||||
@@ -402,16 +401,16 @@ describe("[data] WithBuilder", async () => {
|
|||||||
{
|
{
|
||||||
content: "comment3",
|
content: "comment3",
|
||||||
users: {
|
users: {
|
||||||
username: "user1"
|
username: "user1",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
content: "comment4",
|
content: "comment4",
|
||||||
users: {
|
users: {
|
||||||
username: "user2"
|
username: "user2",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "post3",
|
title: "post3",
|
||||||
@@ -419,17 +418,17 @@ describe("[data] WithBuilder", async () => {
|
|||||||
{
|
{
|
||||||
content: "comment5",
|
content: "comment5",
|
||||||
users: {
|
users: {
|
||||||
username: "user1"
|
username: "user1",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
content: "comment6",
|
content: "comment6",
|
||||||
users: {
|
users: {
|
||||||
username: "user2"
|
username: "user2",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
//console.log(_jsonp(result.data));
|
//console.log(_jsonp(result.data));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ describe("Connection", async () => {
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
name: "name",
|
name: "name",
|
||||||
order: 0
|
order: 0,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -54,14 +54,14 @@ describe("Connection", async () => {
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
name: "name",
|
name: "name",
|
||||||
order: 0
|
order: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "desc",
|
name: "desc",
|
||||||
order: 1
|
order: 1,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,10 +83,10 @@ describe("Connection", async () => {
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
name: "name",
|
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,9 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { BooleanField } from "../../../../src/data";
|
import { BooleanField } from "../../../../src/data";
|
||||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
describe("[data] BooleanField", async () => {
|
describe("[data] BooleanField", async () => {
|
||||||
runBaseFieldTests(BooleanField, { defaultValue: true, schemaType: "boolean" });
|
fieldTestSuite({ expect, test }, BooleanField, { defaultValue: true, schemaType: "boolean" });
|
||||||
|
|
||||||
test("transformRetrieve", async () => {
|
test("transformRetrieve", async () => {
|
||||||
const field = new BooleanField("test");
|
const field = new BooleanField("test");
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { DateField } from "../../../../src/data";
|
import { DateField } from "../../../../src/data";
|
||||||
import { runBaseFieldTests } from "./inc";
|
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
describe("[data] DateField", async () => {
|
describe("[data] DateField", async () => {
|
||||||
runBaseFieldTests(DateField, { defaultValue: new Date(), schemaType: "date" });
|
fieldTestSuite({ expect, test }, DateField, { defaultValue: new Date(), schemaType: "date" });
|
||||||
|
|
||||||
// @todo: add datefield tests
|
// @todo: add datefield tests
|
||||||
test("week", async () => {
|
test("week", async () => {
|
||||||
|
|||||||
@@ -1,25 +1,29 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { EnumField } from "../../../../src/data";
|
import { EnumField } from "../../../../src/data";
|
||||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
function options(strings: string[]) {
|
function options(strings: string[]) {
|
||||||
return { type: "strings", values: strings };
|
return { type: "strings", values: strings };
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("[data] EnumField", async () => {
|
describe("[data] EnumField", async () => {
|
||||||
runBaseFieldTests(
|
fieldTestSuite(
|
||||||
|
{ expect, test },
|
||||||
|
// @ts-ignore
|
||||||
EnumField,
|
EnumField,
|
||||||
{ defaultValue: "a", schemaType: "text" },
|
{ defaultValue: "a", schemaType: "text" },
|
||||||
{ options: options(["a", "b", "c"]) }
|
{ options: options(["a", "b", "c"]) },
|
||||||
);
|
);
|
||||||
|
|
||||||
test("yields if default value is not a valid option", async () => {
|
test("yields if default value is not a valid option", async () => {
|
||||||
expect(
|
expect(
|
||||||
() => new EnumField("test", { options: options(["a", "b"]), default_value: "c" })
|
// @ts-ignore
|
||||||
|
() => new EnumField("test", { options: options(["a", "b"]), default_value: "c" }),
|
||||||
).toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("transformPersist (config)", async () => {
|
test("transformPersist (config)", async () => {
|
||||||
|
// @ts-ignore
|
||||||
const field = new EnumField("test", { options: options(["a", "b", "c"]) });
|
const field = new EnumField("test", { options: options(["a", "b", "c"]) });
|
||||||
|
|
||||||
expect(transformPersist(field, null)).resolves.toBeUndefined();
|
expect(transformPersist(field, null)).resolves.toBeUndefined();
|
||||||
@@ -29,9 +33,10 @@ describe("[data] EnumField", async () => {
|
|||||||
|
|
||||||
test("transformRetrieve", async () => {
|
test("transformRetrieve", async () => {
|
||||||
const field = new EnumField("test", {
|
const field = new EnumField("test", {
|
||||||
|
// @ts-ignore
|
||||||
options: options(["a", "b", "c"]),
|
options: options(["a", "b", "c"]),
|
||||||
default_value: "a",
|
default_value: "a",
|
||||||
required: true
|
required: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(field.transformRetrieve(null)).toBe("a");
|
expect(field.transformRetrieve(null)).toBe("a");
|
||||||
|
|||||||
@@ -1,43 +1,49 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Default, parse, stripMark } from "../../../../src/core/utils";
|
import { Default, stripMark } from "../../../../src/core/utils";
|
||||||
import { Field, type SchemaResponse, TextField, baseFieldConfigSchema } from "../../../../src/data";
|
import { baseFieldConfigSchema, Field } from "../../../../src/data/fields/Field";
|
||||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
describe("[data] Field", async () => {
|
describe("[data] Field", async () => {
|
||||||
class FieldSpec extends Field {
|
class FieldSpec extends Field {
|
||||||
schema(): SchemaResponse {
|
|
||||||
return this.useSchemaHelper("text");
|
|
||||||
}
|
|
||||||
getSchema() {
|
getSchema() {
|
||||||
return baseFieldConfigSchema;
|
return baseFieldConfigSchema;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
runBaseFieldTests(FieldSpec, { defaultValue: "test", schemaType: "text" });
|
test("fieldSpec", () => {
|
||||||
|
expect(new FieldSpec("test").schema()).toEqual({
|
||||||
|
name: "test",
|
||||||
|
type: "text",
|
||||||
|
nullable: true, // always true
|
||||||
|
dflt: undefined, // never using default value
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
fieldTestSuite({ expect, test }, FieldSpec, { defaultValue: "test", schemaType: "text" });
|
||||||
|
|
||||||
test("default config", async () => {
|
test("default config", async () => {
|
||||||
const config = Default(baseFieldConfigSchema, {});
|
const config = Default(baseFieldConfigSchema, {});
|
||||||
expect(stripMark(new FieldSpec("test").config)).toEqual(config);
|
expect(stripMark(new FieldSpec("test").config)).toEqual(config as any);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("transformPersist (specific)", async () => {
|
test("transformPersist (specific)", async () => {
|
||||||
const required = new FieldSpec("test", { required: true });
|
const required = new FieldSpec("test", { required: true });
|
||||||
const requiredDefault = new FieldSpec("test", {
|
const requiredDefault = new FieldSpec("test", {
|
||||||
required: true,
|
required: true,
|
||||||
default_value: "test"
|
default_value: "test",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(required.transformPersist(null, undefined as any, undefined as any)).rejects.toThrow();
|
expect(required.transformPersist(null, undefined as any, undefined as any)).rejects.toThrow();
|
||||||
expect(
|
expect(
|
||||||
required.transformPersist(undefined, undefined as any, undefined as any)
|
required.transformPersist(undefined, undefined as any, undefined as any),
|
||||||
).rejects.toThrow();
|
).rejects.toThrow();
|
||||||
|
|
||||||
// works because it has a default value
|
// works because it has a default value
|
||||||
expect(
|
expect(
|
||||||
requiredDefault.transformPersist(null, undefined as any, undefined as any)
|
requiredDefault.transformPersist(null, undefined as any, undefined as any),
|
||||||
).resolves.toBeDefined();
|
).resolves.toBeDefined();
|
||||||
expect(
|
expect(
|
||||||
requiredDefault.transformPersist(undefined, undefined as any, undefined as any)
|
requiredDefault.transformPersist(undefined, undefined as any, undefined as any),
|
||||||
).resolves.toBeDefined();
|
).resolves.toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Type } from "../../../../src/core/utils";
|
import { Type } from "../../../../src/core/utils";
|
||||||
import {
|
import { Entity, EntityIndex, Field } from "../../../../src/data";
|
||||||
Entity,
|
|
||||||
EntityIndex,
|
|
||||||
type EntityManager,
|
|
||||||
Field,
|
|
||||||
type SchemaResponse
|
|
||||||
} from "../../../../src/data";
|
|
||||||
|
|
||||||
class TestField extends Field {
|
class TestField extends Field {
|
||||||
protected getSchema(): any {
|
protected getSchema(): any {
|
||||||
return Type.Any();
|
return Type.Any();
|
||||||
}
|
}
|
||||||
|
|
||||||
schema(em: EntityManager<any>): SchemaResponse {
|
override schema() {
|
||||||
return undefined as any;
|
return undefined as any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { JsonField } from "../../../../src/data";
|
import { JsonField } from "../../../../src/data";
|
||||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
describe("[data] JsonField", async () => {
|
describe("[data] JsonField", async () => {
|
||||||
const field = new JsonField("test");
|
const field = new JsonField("test");
|
||||||
runBaseFieldTests(JsonField, {
|
fieldTestSuite({ expect, test }, JsonField, {
|
||||||
defaultValue: { a: 1 },
|
defaultValue: { a: 1 },
|
||||||
sampleValues: ["string", { test: 1 }, 1],
|
sampleValues: ["string", { test: 1 }, 1],
|
||||||
schemaType: "text"
|
schemaType: "text",
|
||||||
});
|
});
|
||||||
|
|
||||||
test("transformPersist (no config)", async () => {
|
test("transformPersist (no config)", async () => {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { JsonSchemaField } from "../../../../src/data";
|
import { JsonSchemaField } from "../../../../src/data";
|
||||||
import { runBaseFieldTests } from "./inc";
|
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
describe("[data] JsonSchemaField", async () => {
|
describe("[data] JsonSchemaField", async () => {
|
||||||
runBaseFieldTests(JsonSchemaField, { defaultValue: {}, schemaType: "text" });
|
// @ts-ignore
|
||||||
|
fieldTestSuite({ expect, test }, JsonSchemaField, { defaultValue: {}, schemaType: "text" });
|
||||||
|
|
||||||
// @todo: add JsonSchemaField tests
|
// @todo: add JsonSchemaField tests
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { NumberField } from "../../../../src/data";
|
import { NumberField } from "../../../../src/data";
|
||||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
describe("[data] NumberField", async () => {
|
describe("[data] NumberField", async () => {
|
||||||
test("transformPersist (config)", async () => {
|
test("transformPersist (config)", async () => {
|
||||||
@@ -15,5 +15,5 @@ describe("[data] NumberField", async () => {
|
|||||||
expect(transformPersist(field2, 10000)).resolves.toBe(10000);
|
expect(transformPersist(field2, 10000)).resolves.toBe(10000);
|
||||||
});
|
});
|
||||||
|
|
||||||
runBaseFieldTests(NumberField, { defaultValue: 12, schemaType: "integer" });
|
fieldTestSuite({ expect, test }, NumberField, { defaultValue: 12, schemaType: "integer" });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ describe("[data] PrimaryField", async () => {
|
|||||||
|
|
||||||
test("schema", () => {
|
test("schema", () => {
|
||||||
expect(field.name).toBe("primary");
|
expect(field.name).toBe("primary");
|
||||||
expect(field.schema()).toEqual(["primary", "integer", expect.any(Function)]);
|
expect(field.schema()).toEqual({
|
||||||
|
name: "primary",
|
||||||
|
type: "integer" as const,
|
||||||
|
nullable: false,
|
||||||
|
primary: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("hasDefault", async () => {
|
test("hasDefault", async () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { TextField } from "../../../../src/data";
|
import { TextField } from "../../../../src/data";
|
||||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
describe("[data] TextField", async () => {
|
describe("[data] TextField", async () => {
|
||||||
test("transformPersist (config)", async () => {
|
test("transformPersist (config)", async () => {
|
||||||
@@ -11,5 +11,5 @@ describe("[data] TextField", async () => {
|
|||||||
expect(transformPersist(field, "abc")).resolves.toBe("abc");
|
expect(transformPersist(field, "abc")).resolves.toBe("abc");
|
||||||
});
|
});
|
||||||
|
|
||||||
runBaseFieldTests(TextField, { defaultValue: "abc", schemaType: "text" });
|
fieldTestSuite({ expect, test }, TextField, { defaultValue: "abc", schemaType: "text" });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
type BaseRelationConfig,
|
type BaseRelationConfig,
|
||||||
EntityRelation,
|
EntityRelation,
|
||||||
EntityRelationAnchor,
|
EntityRelationAnchor,
|
||||||
RelationTypes
|
RelationTypes,
|
||||||
} from "../../../../src/data/relations";
|
} from "../../../../src/data/relations";
|
||||||
|
|
||||||
class TestEntityRelation extends EntityRelation {
|
class TestEntityRelation extends EntityRelation {
|
||||||
@@ -12,7 +12,7 @@ class TestEntityRelation extends EntityRelation {
|
|||||||
super(
|
super(
|
||||||
new EntityRelationAnchor(new Entity("source"), "source"),
|
new EntityRelationAnchor(new Entity("source"), "source"),
|
||||||
new EntityRelationAnchor(new Entity("target"), "target"),
|
new EntityRelationAnchor(new Entity("target"), "target"),
|
||||||
config
|
config,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
initialize(em: EntityManager<any>) {}
|
initialize(em: EntityManager<any>) {}
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ beforeAll(() =>
|
|||||||
method: init?.method ?? "GET",
|
method: init?.method ?? "GET",
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
headers: Object.fromEntries(init?.headers?.entries() ?? []),
|
headers: Object.fromEntries(init?.headers?.entries() ?? []),
|
||||||
body: init?.body
|
body: init?.body,
|
||||||
};
|
};
|
||||||
|
|
||||||
return new Response(JSON.stringify({ todos: [1, 2], request }), {
|
return new Response(JSON.stringify({ todos: [1, 2], request }), {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { "Content-Type": "application/json" }
|
headers: { "Content-Type": "application/json" },
|
||||||
});
|
});
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
afterAll(unmockFetch);
|
afterAll(unmockFetch);
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ describe("FetchTask", async () => {
|
|||||||
const task = new FetchTask("Fetch Something", {
|
const task = new FetchTask("Fetch Something", {
|
||||||
url: "https://jsonplaceholder.typicode.com/todos/1",
|
url: "https://jsonplaceholder.typicode.com/todos/1",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: [{ key: "Content-Type", value: "application/json" }]
|
headers: [{ key: "Content-Type", value: "application/json" }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await task.run();
|
const result = await task.run();
|
||||||
@@ -62,18 +62,18 @@ describe("FetchTask", async () => {
|
|||||||
|
|
||||||
expect(
|
expect(
|
||||||
// // @ts-expect-error
|
// // @ts-expect-error
|
||||||
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: 1 })
|
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: 1 }),
|
||||||
).toThrow();
|
).toThrow();
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
new FetchTask("", {
|
new FetchTask("", {
|
||||||
url: "https://jsonplaceholder.typicode.com",
|
url: "https://jsonplaceholder.typicode.com",
|
||||||
method: "invalid"
|
method: "invalid",
|
||||||
}).execute()
|
}).execute(),
|
||||||
).rejects.toThrow(/^Invalid method/);
|
).rejects.toThrow(/^Invalid method/);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: "GET" })
|
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: "GET" }),
|
||||||
).toBeDefined();
|
).toBeDefined();
|
||||||
|
|
||||||
expect(() => new FetchTask("", { url: "", method: "Invalid" })).toThrow();
|
expect(() => new FetchTask("", { url: "", method: "Invalid" })).toThrow();
|
||||||
@@ -85,17 +85,17 @@ describe("FetchTask", async () => {
|
|||||||
method: "{{ flow.output.method }}",
|
method: "{{ flow.output.method }}",
|
||||||
headers: [
|
headers: [
|
||||||
{ key: "Content-{{ flow.output.headerKey }}", value: "application/json" },
|
{ 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({
|
body: JSON.stringify({
|
||||||
email: "{{ flow.output.email }}"
|
email: "{{ flow.output.email }}",
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
const inputs = {
|
const inputs = {
|
||||||
headerKey: "Type",
|
headerKey: "Type",
|
||||||
apiKey: 123,
|
apiKey: 123,
|
||||||
email: "what@else.com",
|
email: "what@else.com",
|
||||||
method: "PATCH"
|
method: "PATCH",
|
||||||
};
|
};
|
||||||
|
|
||||||
const flow = new Flow("", [task]);
|
const flow = new Flow("", [task]);
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ import { Flow, LogTask, RenderTask, SubFlowTask } from "../../src/flows";
|
|||||||
describe("SubFlowTask", async () => {
|
describe("SubFlowTask", async () => {
|
||||||
test("Simple Subflow", async () => {
|
test("Simple Subflow", async () => {
|
||||||
const subTask = new RenderTask("render", {
|
const subTask = new RenderTask("render", {
|
||||||
render: "subflow"
|
render: "subflow",
|
||||||
});
|
});
|
||||||
const subflow = new Flow("subflow", [subTask]);
|
const subflow = new Flow("subflow", [subTask]);
|
||||||
|
|
||||||
const task = new LogTask("log");
|
const task = new LogTask("log");
|
||||||
const task2 = new SubFlowTask("sub", {
|
const task2 = new SubFlowTask("sub", {
|
||||||
flow: subflow
|
flow: subflow,
|
||||||
});
|
});
|
||||||
const task3 = new RenderTask("render2", {
|
const task3 = new RenderTask("render2", {
|
||||||
render: "Subflow output: {{ sub.output }}"
|
render: "Subflow output: {{ sub.output }}",
|
||||||
});
|
});
|
||||||
|
|
||||||
const flow = new Flow("test", [task, task2, task3], []);
|
const flow = new Flow("test", [task, task2, task3], []);
|
||||||
@@ -30,7 +30,7 @@ describe("SubFlowTask", async () => {
|
|||||||
|
|
||||||
test("Simple loop", async () => {
|
test("Simple loop", async () => {
|
||||||
const subTask = new RenderTask("render", {
|
const subTask = new RenderTask("render", {
|
||||||
render: "run {{ flow.output }}"
|
render: "run {{ flow.output }}",
|
||||||
});
|
});
|
||||||
const subflow = new Flow("subflow", [subTask]);
|
const subflow = new Flow("subflow", [subTask]);
|
||||||
|
|
||||||
@@ -38,10 +38,10 @@ describe("SubFlowTask", async () => {
|
|||||||
const task2 = new SubFlowTask("sub", {
|
const task2 = new SubFlowTask("sub", {
|
||||||
flow: subflow,
|
flow: subflow,
|
||||||
loop: true,
|
loop: true,
|
||||||
input: [1, 2, 3]
|
input: [1, 2, 3],
|
||||||
});
|
});
|
||||||
const task3 = new RenderTask("render2", {
|
const task3 = new RenderTask("render2", {
|
||||||
render: `Subflow output: {{ sub.output | join: ", " }}`
|
render: `Subflow output: {{ sub.output | join: ", " }}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const flow = new Flow("test", [task, task2, task3], []);
|
const flow = new Flow("test", [task, task2, task3], []);
|
||||||
@@ -61,7 +61,7 @@ describe("SubFlowTask", async () => {
|
|||||||
|
|
||||||
test("Simple loop from flow input", async () => {
|
test("Simple loop from flow input", async () => {
|
||||||
const subTask = new RenderTask("render", {
|
const subTask = new RenderTask("render", {
|
||||||
render: "run {{ flow.output }}"
|
render: "run {{ flow.output }}",
|
||||||
});
|
});
|
||||||
|
|
||||||
const subflow = new Flow("subflow", [subTask]);
|
const subflow = new Flow("subflow", [subTask]);
|
||||||
@@ -70,10 +70,10 @@ describe("SubFlowTask", async () => {
|
|||||||
const task2 = new SubFlowTask("sub", {
|
const task2 = new SubFlowTask("sub", {
|
||||||
flow: subflow,
|
flow: subflow,
|
||||||
loop: true,
|
loop: true,
|
||||||
input: "{{ flow.output | json }}"
|
input: "{{ flow.output | json }}",
|
||||||
});
|
});
|
||||||
const task3 = new RenderTask("render2", {
|
const task3 = new RenderTask("render2", {
|
||||||
render: `Subflow output: {{ sub.output | join: ", " }}`
|
render: `Subflow output: {{ sub.output | join: ", " }}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const flow = new Flow("test", [task, task2, task3], []);
|
const flow = new Flow("test", [task, task2, task3], []);
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ describe("Task", async () => {
|
|||||||
const result = await Task.resolveParams(
|
const result = await Task.resolveParams(
|
||||||
Type.Object({ test: dynamic(Type.Number()) }),
|
Type.Object({ test: dynamic(Type.Number()) }),
|
||||||
{
|
{
|
||||||
test: "{{ some.path }}"
|
test: "{{ some.path }}",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
some: {
|
some: {
|
||||||
path: 1
|
path: 1,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.test).toBe(1);
|
expect(result.test).toBe(1);
|
||||||
@@ -24,13 +24,13 @@ describe("Task", async () => {
|
|||||||
const result = await Task.resolveParams(
|
const result = await Task.resolveParams(
|
||||||
Type.Object({ test: Type.String() }),
|
Type.Object({ test: Type.String() }),
|
||||||
{
|
{
|
||||||
test: "{{ some.path }}"
|
test: "{{ some.path }}",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
some: {
|
some: {
|
||||||
path: "1/1"
|
path: "1/1",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.test).toBe("1/1");
|
expect(result.test).toBe("1/1");
|
||||||
@@ -40,13 +40,13 @@ describe("Task", async () => {
|
|||||||
const result = await Task.resolveParams(
|
const result = await Task.resolveParams(
|
||||||
Type.Object({ test: dynamic(Type.Object({ key: Type.String(), value: Type.String() })) }),
|
Type.Object({ test: dynamic(Type.Object({ key: Type.String(), value: Type.String() })) }),
|
||||||
{
|
{
|
||||||
test: { key: "path", value: "{{ some.path }}" }
|
test: { key: "path", value: "{{ some.path }}" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
some: {
|
some: {
|
||||||
path: "1/1"
|
path: "1/1",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.test).toEqual({ key: "path", value: "1/1" });
|
expect(result.test).toEqual({ key: "path", value: "1/1" });
|
||||||
@@ -55,17 +55,17 @@ describe("Task", async () => {
|
|||||||
test("resolveParams: with json", async () => {
|
test("resolveParams: with json", async () => {
|
||||||
const result = await Task.resolveParams(
|
const result = await Task.resolveParams(
|
||||||
Type.Object({
|
Type.Object({
|
||||||
test: dynamic(Type.Object({ key: Type.String(), value: Type.String() }))
|
test: dynamic(Type.Object({ key: Type.String(), value: Type.String() })),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
test: "{{ some | json }}"
|
test: "{{ some | json }}",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
some: {
|
some: {
|
||||||
key: "path",
|
key: "path",
|
||||||
value: "1/1"
|
value: "1/1",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.test).toEqual({ key: "path", value: "1/1" });
|
expect(result.test).toEqual({ key: "path", value: "1/1" });
|
||||||
@@ -74,11 +74,11 @@ describe("Task", async () => {
|
|||||||
test("resolveParams: with array", async () => {
|
test("resolveParams: with array", async () => {
|
||||||
const result = await Task.resolveParams(
|
const result = await Task.resolveParams(
|
||||||
Type.Object({
|
Type.Object({
|
||||||
test: dynamic(Type.Array(Type.String()))
|
test: dynamic(Type.Array(Type.String())),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
test: '{{ "1,2,3" | split: "," | json }}'
|
test: '{{ "1,2,3" | split: "," | json }}',
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.test).toEqual(["1", "2", "3"]);
|
expect(result.test).toEqual(["1", "2", "3"]);
|
||||||
@@ -87,11 +87,11 @@ describe("Task", async () => {
|
|||||||
test("resolveParams: boolean", async () => {
|
test("resolveParams: boolean", async () => {
|
||||||
const result = await Task.resolveParams(
|
const result = await Task.resolveParams(
|
||||||
Type.Object({
|
Type.Object({
|
||||||
test: dynamic(Type.Boolean())
|
test: dynamic(Type.Boolean()),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
test: "{{ true }}"
|
test: "{{ true }}",
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.test).toEqual(true);
|
expect(result.test).toEqual(true);
|
||||||
@@ -100,11 +100,11 @@ describe("Task", async () => {
|
|||||||
test("resolveParams: float", async () => {
|
test("resolveParams: float", async () => {
|
||||||
const result = await Task.resolveParams(
|
const result = await Task.resolveParams(
|
||||||
Type.Object({
|
Type.Object({
|
||||||
test: dynamic(Type.Number(), Number.parseFloat)
|
test: dynamic(Type.Number(), Number.parseFloat),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
test: "{{ 3.14 }}"
|
test: "{{ 3.14 }}",
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.test).toEqual(3.14);
|
expect(result.test).toEqual(3.14);
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ const first = getNamedTask(
|
|||||||
//throw new Error("Error");
|
//throw new Error("Error");
|
||||||
return {
|
return {
|
||||||
inner: {
|
inner: {
|
||||||
result: 2
|
result: 2,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
1000
|
1000,
|
||||||
);
|
);
|
||||||
const second = getNamedTask("second (if match)");
|
const second = getNamedTask("second (if match)");
|
||||||
const third = getNamedTask("third (if error)");
|
const third = getNamedTask("third (if error)");
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class ExecTask extends Task {
|
|||||||
constructor(
|
constructor(
|
||||||
name: string,
|
name: string,
|
||||||
params: any,
|
params: any,
|
||||||
private fn: () => any
|
private fn: () => any,
|
||||||
) {
|
) {
|
||||||
super(name, params);
|
super(name, params);
|
||||||
}
|
}
|
||||||
@@ -54,8 +54,8 @@ export function getNamedTask(name: string, _func?: () => Promise<any>, delay?: n
|
|||||||
return new ExecTask(
|
return new ExecTask(
|
||||||
name,
|
name,
|
||||||
{
|
{
|
||||||
delay
|
delay,
|
||||||
},
|
},
|
||||||
func
|
func,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const first = new LogTask("First", { delay: 1000 });
|
|||||||
const second = new LogTask("Second", { delay: 1000 });
|
const second = new LogTask("Second", { delay: 1000 });
|
||||||
const third = new LogTask("Long Third", { delay: 2500 });
|
const third = new LogTask("Long Third", { delay: 2500 });
|
||||||
const fourth = new FetchTask("Fetch Something", {
|
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
|
const fifth = new LogTask("Task 4", { delay: 500 }); // without connection
|
||||||
|
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ class OutputParamTask extends Task<typeof OutputParamTask.schema> {
|
|||||||
static override schema = Type.Object({
|
static override schema = Type.Object({
|
||||||
number: dynamic(
|
number: dynamic(
|
||||||
Type.Number({
|
Type.Number({
|
||||||
title: "Output number"
|
title: "Output number",
|
||||||
}),
|
}),
|
||||||
Number.parseInt
|
Number.parseInt,
|
||||||
)
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
async execute(inputs: InputsMap) {
|
async execute(inputs: InputsMap) {
|
||||||
@@ -75,7 +75,7 @@ describe("Flow task inputs", async () => {
|
|||||||
test("output/input", async () => {
|
test("output/input", async () => {
|
||||||
const task = new OutputParamTask("task1", { number: 111 });
|
const task = new OutputParamTask("task1", { number: 111 });
|
||||||
const task2 = new OutputParamTask("task2", {
|
const task2 = new OutputParamTask("task2", {
|
||||||
number: "{{ task1.output }}"
|
number: "{{ task1.output }}",
|
||||||
});
|
});
|
||||||
|
|
||||||
const flow = new Flow("test", [task, task2]);
|
const flow = new Flow("test", [task, task2]);
|
||||||
@@ -94,10 +94,10 @@ describe("Flow task inputs", async () => {
|
|||||||
|
|
||||||
test("input from flow", async () => {
|
test("input from flow", async () => {
|
||||||
const task = new OutputParamTask("task1", {
|
const task = new OutputParamTask("task1", {
|
||||||
number: "{{flow.output.someFancyParam}}"
|
number: "{{flow.output.someFancyParam}}",
|
||||||
});
|
});
|
||||||
const task2 = new OutputParamTask("task2", {
|
const task2 = new OutputParamTask("task2", {
|
||||||
number: "{{task1.output}}"
|
number: "{{task1.output}}",
|
||||||
});
|
});
|
||||||
|
|
||||||
const flow = new Flow("test", [task, task2]);
|
const flow = new Flow("test", [task, task2]);
|
||||||
@@ -126,7 +126,7 @@ describe("Flow task inputs", async () => {
|
|||||||
const emgr = new EventManager({ EventTriggerClass });
|
const emgr = new EventManager({ EventTriggerClass });
|
||||||
|
|
||||||
const task = new OutputParamTask("event", {
|
const task = new OutputParamTask("event", {
|
||||||
number: "{{flow.output.number}}"
|
number: "{{flow.output.number}}",
|
||||||
});
|
});
|
||||||
const flow = new Flow(
|
const flow = new Flow(
|
||||||
"test",
|
"test",
|
||||||
@@ -134,8 +134,8 @@ describe("Flow task inputs", async () => {
|
|||||||
[],
|
[],
|
||||||
new EventTrigger({
|
new EventTrigger({
|
||||||
event: "test-event",
|
event: "test-event",
|
||||||
mode: "sync"
|
mode: "sync",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
flow.setRespondingTask(task);
|
flow.setRespondingTask(task);
|
||||||
flow.trigger.register(flow, emgr);
|
flow.trigger.register(flow, emgr);
|
||||||
@@ -155,8 +155,8 @@ describe("Flow task inputs", async () => {
|
|||||||
new HttpTrigger({
|
new HttpTrigger({
|
||||||
path: "/test",
|
path: "/test",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
mode: "sync"
|
mode: "sync",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
flow.setRespondingTask(task);
|
flow.setRespondingTask(task);
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const flows = {
|
|||||||
back,
|
back,
|
||||||
fanout,
|
fanout,
|
||||||
parallel,
|
parallel,
|
||||||
simpleFetch
|
simpleFetch,
|
||||||
};
|
};
|
||||||
|
|
||||||
const arg = process.argv[2];
|
const arg = process.argv[2];
|
||||||
@@ -32,7 +32,7 @@ const colors = [
|
|||||||
"#F78F1E", // Saffron
|
"#F78F1E", // Saffron
|
||||||
"#BD10E0", // Vivid Purple
|
"#BD10E0", // Vivid Purple
|
||||||
"#50E3C2", // Turquoise
|
"#50E3C2", // Turquoise
|
||||||
"#9013FE" // Grape
|
"#9013FE", // Grape
|
||||||
];
|
];
|
||||||
|
|
||||||
const colorsCache: Record<string, string> = {};
|
const colorsCache: Record<string, string> = {};
|
||||||
@@ -82,7 +82,7 @@ function TerminalFlow({ flow }: { flow: Flow }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return t;
|
return t;
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -92,7 +92,7 @@ function TerminalFlow({ flow }: { flow: Flow }) {
|
|||||||
console.log("done", response ? response : "(no response)");
|
console.log("done", response ? response : "(no response)");
|
||||||
console.log(
|
console.log(
|
||||||
"Executed tasks:",
|
"Executed tasks:",
|
||||||
execution.logs.map((l) => l.task.name)
|
execution.logs.map((l) => l.task.name),
|
||||||
);
|
);
|
||||||
console.log("Executed count:", execution.logs.length);
|
console.log("Executed count:", execution.logs.length);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class ExecTask extends Task {
|
|||||||
constructor(
|
constructor(
|
||||||
name: string,
|
name: string,
|
||||||
params: any,
|
params: any,
|
||||||
private fn: () => any
|
private fn: () => any,
|
||||||
) {
|
) {
|
||||||
super(name, params);
|
super(name, params);
|
||||||
}
|
}
|
||||||
@@ -60,7 +60,7 @@ describe("Flow trigger", async () => {
|
|||||||
"test",
|
"test",
|
||||||
[task],
|
[task],
|
||||||
[],
|
[],
|
||||||
new EventTrigger({ event: "test-event", mode: "sync" })
|
new EventTrigger({ event: "test-event", mode: "sync" }),
|
||||||
);
|
);
|
||||||
|
|
||||||
flow.trigger.register(flow, emgr);
|
flow.trigger.register(flow, emgr);
|
||||||
@@ -107,8 +107,8 @@ describe("Flow trigger", async () => {
|
|||||||
new HttpTrigger({
|
new HttpTrigger({
|
||||||
path: "/test",
|
path: "/test",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
mode: "sync"
|
mode: "sync",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const hono = new Hono();
|
const hono = new Hono();
|
||||||
@@ -123,7 +123,7 @@ describe("Flow trigger", async () => {
|
|||||||
|
|
||||||
test("http trigger with response", async () => {
|
test("http trigger with response", async () => {
|
||||||
const task = ExecTask.create("http", () => ({
|
const task = ExecTask.create("http", () => ({
|
||||||
called: true
|
called: true,
|
||||||
}));
|
}));
|
||||||
const flow = new Flow(
|
const flow = new Flow(
|
||||||
"test",
|
"test",
|
||||||
@@ -132,8 +132,8 @@ describe("Flow trigger", async () => {
|
|||||||
new HttpTrigger({
|
new HttpTrigger({
|
||||||
path: "/test",
|
path: "/test",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
mode: "sync"
|
mode: "sync",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
flow.setRespondingTask(task);
|
flow.setRespondingTask(task);
|
||||||
|
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ class ExecTask extends Task<typeof ExecTask.schema> {
|
|||||||
type = "exec";
|
type = "exec";
|
||||||
|
|
||||||
static override schema = Type.Object({
|
static override schema = Type.Object({
|
||||||
delay: Type.Number({ default: 10 })
|
delay: Type.Number({ default: 10 }),
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
name: string,
|
name: string,
|
||||||
params: Static<typeof ExecTask.schema>,
|
params: Static<typeof ExecTask.schema>,
|
||||||
private func: () => Promise<any>
|
private func: () => Promise<any>,
|
||||||
) {
|
) {
|
||||||
super(name, params);
|
super(name, params);
|
||||||
}
|
}
|
||||||
@@ -36,12 +36,12 @@ function getTask(num: number = 0, delay: number = 5) {
|
|||||||
return new ExecTask(
|
return new ExecTask(
|
||||||
`Task ${num}`,
|
`Task ${num}`,
|
||||||
{
|
{
|
||||||
delay
|
delay,
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
//console.log(`[DONE] Task: ${num}`);
|
//console.log(`[DONE] Task: ${num}`);
|
||||||
return true;
|
return true;
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
//return new LogTask(`Log ${num}`, { delay });
|
//return new LogTask(`Log ${num}`, { delay });
|
||||||
}
|
}
|
||||||
@@ -56,9 +56,9 @@ function getNamedTask(name: string, _func?: () => Promise<any>, delay?: number)
|
|||||||
return new ExecTask(
|
return new ExecTask(
|
||||||
name,
|
name,
|
||||||
{
|
{
|
||||||
delay: delay ?? 0
|
delay: delay ?? 0,
|
||||||
},
|
},
|
||||||
func
|
func,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +228,7 @@ describe("Flow tests", async () => {
|
|||||||
back
|
back
|
||||||
.task(third)
|
.task(third)
|
||||||
.getOutTasks()
|
.getOutTasks()
|
||||||
.map((t) => t.name)
|
.map((t) => t.name),
|
||||||
).toEqual(["second", "fourth"]);
|
).toEqual(["second", "fourth"]);
|
||||||
|
|
||||||
const execution = back.createExecution();
|
const execution = back.createExecution();
|
||||||
@@ -263,7 +263,7 @@ describe("Flow tests", async () => {
|
|||||||
back
|
back
|
||||||
.task(third)
|
.task(third)
|
||||||
.getOutTasks()
|
.getOutTasks()
|
||||||
.map((t) => t.name)
|
.map((t) => t.name),
|
||||||
).toEqual(["second", "fourth"]);
|
).toEqual(["second", "fourth"]);
|
||||||
|
|
||||||
const execution = back.createExecution();
|
const execution = back.createExecution();
|
||||||
@@ -324,8 +324,8 @@ describe("Flow tests", async () => {
|
|||||||
const first = getNamedTask("first", async () => {
|
const first = getNamedTask("first", async () => {
|
||||||
return {
|
return {
|
||||||
inner: {
|
inner: {
|
||||||
result: 2
|
result: 2,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const second = getNamedTask("second");
|
const second = getNamedTask("second");
|
||||||
@@ -361,7 +361,7 @@ describe("Flow tests", async () => {
|
|||||||
"[event]",
|
"[event]",
|
||||||
event.isStart() ? "start" : "end",
|
event.isStart() ? "start" : "end",
|
||||||
event.task().name,
|
event.task().name,
|
||||||
event.isStart() ? undefined : event.succeeded()
|
event.isStart() ? undefined : event.succeeded(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -389,7 +389,7 @@ describe("Flow tests", async () => {
|
|||||||
const second = new LogTask("Task 1");
|
const second = new LogTask("Task 1");
|
||||||
const third = new LogTask("Task 2", { delay: 50 });
|
const third = new LogTask("Task 2", { delay: 50 });
|
||||||
const fourth = new FetchTask("Fetch Something", {
|
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
|
const fifth = new LogTask("Task 4"); // without connection
|
||||||
|
|
||||||
@@ -405,7 +405,7 @@ describe("Flow tests", async () => {
|
|||||||
// @todo: fix
|
// @todo: fix
|
||||||
const deserialized = Flow.fromObject("", original, {
|
const deserialized = Flow.fromObject("", original, {
|
||||||
fetch: { cls: FetchTask },
|
fetch: { cls: FetchTask },
|
||||||
log: { cls: LogTask }
|
log: { cls: LogTask },
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
const diffdeep = getObjectDiff(original, deserialized.toJSON());
|
const diffdeep = getObjectDiff(original, deserialized.toJSON());
|
||||||
@@ -414,7 +414,7 @@ describe("Flow tests", async () => {
|
|||||||
expect(flow.startTask.name).toEqual(deserialized.startTask.name);
|
expect(flow.startTask.name).toEqual(deserialized.startTask.name);
|
||||||
expect(flow.respondingTask?.name).toEqual(
|
expect(flow.respondingTask?.name).toEqual(
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
deserialized.respondingTask?.name
|
deserialized.respondingTask?.name,
|
||||||
);
|
);
|
||||||
|
|
||||||
//console.log("--- creating original sequence");
|
//console.log("--- creating original sequence");
|
||||||
|
|||||||
+53
-3
@@ -4,6 +4,9 @@ import Database from "libsql";
|
|||||||
import { format as sqlFormat } from "sql-formatter";
|
import { format as sqlFormat } from "sql-formatter";
|
||||||
import { type Connection, EntityManager, SqliteLocalConnection } from "../src/data";
|
import { type Connection, EntityManager, SqliteLocalConnection } from "../src/data";
|
||||||
import type { em as protoEm } from "../src/data/prototype";
|
import type { em as protoEm } from "../src/data/prototype";
|
||||||
|
import { writeFile } from "node:fs/promises";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { slugify } from "core/utils/strings";
|
||||||
|
|
||||||
export function getDummyDatabase(memory: boolean = true): {
|
export function getDummyDatabase(memory: boolean = true): {
|
||||||
dummyDb: SqliteDatabase;
|
dummyDb: SqliteDatabase;
|
||||||
@@ -17,7 +20,7 @@ export function getDummyDatabase(memory: boolean = true): {
|
|||||||
afterAllCleanup: async () => {
|
afterAllCleanup: async () => {
|
||||||
if (!memory) await unlink(DB_NAME);
|
if (!memory) await unlink(DB_NAME);
|
||||||
return true;
|
return true;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +30,7 @@ export function getDummyConnection(memory: boolean = true) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
dummyConnection,
|
dummyConnection,
|
||||||
afterAllCleanup
|
afterAllCleanup,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +42,7 @@ type ConsoleSeverity = "log" | "warn" | "error";
|
|||||||
const _oldConsoles = {
|
const _oldConsoles = {
|
||||||
log: console.log,
|
log: console.log,
|
||||||
warn: console.warn,
|
warn: console.warn,
|
||||||
error: console.error
|
error: console.error,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function disableConsoleLog(severities: ConsoleSeverity[] = ["log", "warn"]) {
|
export function disableConsoleLog(severities: ConsoleSeverity[] = ["log", "warn"]) {
|
||||||
@@ -68,3 +71,50 @@ export function schemaToEm(s: ReturnType<typeof protoEm>, conn?: Connection): En
|
|||||||
const connection = conn ? conn : getDummyConnection().dummyConnection;
|
const connection = conn ? conn : getDummyConnection().dummyConnection;
|
||||||
return new EntityManager(Object.values(s.entities), connection, s.relations, s.indices);
|
return new EntityManager(Object.values(s.entities), connection, s.relations, s.indices);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,8 +1,12 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
import { afterAll, afterEach, beforeAll, describe, expect, it } from "bun:test";
|
||||||
import { App, createApp } from "../../src";
|
import { App, createApp } from "../../src";
|
||||||
import type { AuthResponse } from "../../src/auth";
|
import type { AuthResponse } from "../../src/auth";
|
||||||
|
import { auth } from "../../src/auth/middlewares";
|
||||||
import { randomString, secureRandomString, withDisabledConsole } from "../../src/core/utils";
|
import { randomString, secureRandomString, withDisabledConsole } from "../../src/core/utils";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "../helper";
|
import { disableConsoleLog, enableConsoleLog, getDummyConnection } from "../helper";
|
||||||
|
|
||||||
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
|
afterEach(afterAllCleanup);
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
beforeAll(disableConsoleLog);
|
||||||
afterAll(enableConsoleLog);
|
afterAll(enableConsoleLog);
|
||||||
@@ -15,25 +19,25 @@ const roles = {
|
|||||||
"system.schema.read",
|
"system.schema.read",
|
||||||
"system.access.api",
|
"system.access.api",
|
||||||
"system.config.read",
|
"system.config.read",
|
||||||
"data.entity.read"
|
"data.entity.read",
|
||||||
],
|
],
|
||||||
is_default: true
|
is_default: true,
|
||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
is_default: true,
|
is_default: true,
|
||||||
implicit_allow: true
|
implicit_allow: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
strict: {
|
strict: {
|
||||||
guest: {
|
guest: {
|
||||||
permissions: ["system.access.api", "system.config.read", "data.entity.read"],
|
permissions: ["system.access.api", "system.config.read", "data.entity.read"],
|
||||||
is_default: true
|
is_default: true,
|
||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
is_default: true,
|
is_default: true,
|
||||||
implicit_allow: true
|
implicit_allow: true,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
const configs = {
|
const configs = {
|
||||||
auth: {
|
auth: {
|
||||||
@@ -41,31 +45,32 @@ const configs = {
|
|||||||
entity_name: "users",
|
entity_name: "users",
|
||||||
jwt: {
|
jwt: {
|
||||||
secret: secureRandomString(20),
|
secret: secureRandomString(20),
|
||||||
issuer: randomString(10)
|
issuer: randomString(10),
|
||||||
},
|
},
|
||||||
roles: roles.strict,
|
roles: roles.strict,
|
||||||
guard: {
|
guard: {
|
||||||
enabled: true
|
enabled: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
normal: {
|
normal: {
|
||||||
email: "normal@bknd.io",
|
email: "normal@bknd.io",
|
||||||
password: "12345678"
|
password: "12345678",
|
||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
email: "admin@bknd.io",
|
email: "admin@bknd.io",
|
||||||
password: "12345678",
|
password: "12345678",
|
||||||
role: "admin"
|
role: "admin",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function createAuthApp() {
|
function createAuthApp() {
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
|
connection: dummyConnection,
|
||||||
initialConfig: {
|
initialConfig: {
|
||||||
auth: configs.auth
|
auth: configs.auth,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
app.emgr.onEvent(
|
app.emgr.onEvent(
|
||||||
@@ -74,7 +79,7 @@ function createAuthApp() {
|
|||||||
await app.createUser(configs.users.normal);
|
await app.createUser(configs.users.normal);
|
||||||
await app.createUser(configs.users.admin);
|
await app.createUser(configs.users.admin);
|
||||||
},
|
},
|
||||||
"sync"
|
"sync",
|
||||||
);
|
);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
@@ -93,14 +98,14 @@ const fns = <Mode extends "cookie" | "token" = "token">(app: App, mode?: Mode) =
|
|||||||
if (mode === "cookie") {
|
if (mode === "cookie") {
|
||||||
return {
|
return {
|
||||||
cookie: `auth=${token};`,
|
cookie: `auth=${token};`,
|
||||||
...additional
|
...additional,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: token ? `Bearer ${token}` : "",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...additional
|
...additional,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function body(obj?: Record<string, any>) {
|
function body(obj?: Record<string, any>) {
|
||||||
@@ -117,12 +122,12 @@ const fns = <Mode extends "cookie" | "token" = "token">(app: App, mode?: Mode) =
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
login: async (
|
login: async (
|
||||||
user: any
|
user: any,
|
||||||
): Promise<{ res: Response; data: Mode extends "token" ? AuthResponse : string }> => {
|
): Promise<{ res: Response; data: Mode extends "token" ? AuthResponse : string }> => {
|
||||||
const res = (await app.server.request("/api/auth/password/login", {
|
const res = (await app.server.request("/api/auth/password/login", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: headers(),
|
headers: headers(),
|
||||||
body: body(user)
|
body: body(user),
|
||||||
})) as Response;
|
})) as Response;
|
||||||
|
|
||||||
const data = mode === "cookie" ? getCookie(res, "auth") : await res.json();
|
const data = mode === "cookie" ? getCookie(res, "auth") : await res.json();
|
||||||
@@ -132,10 +137,10 @@ const fns = <Mode extends "cookie" | "token" = "token">(app: App, mode?: Mode) =
|
|||||||
me: async (token?: string): Promise<Pick<AuthResponse, "user">> => {
|
me: async (token?: string): Promise<Pick<AuthResponse, "user">> => {
|
||||||
const res = (await app.server.request("/api/auth/me", {
|
const res = (await app.server.request("/api/auth/me", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: headers(token)
|
headers: headers(token),
|
||||||
})) as Response;
|
})) as Response;
|
||||||
return await res.json();
|
return await res.json();
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -210,4 +215,36 @@ describe("integration auth", () => {
|
|||||||
expect(res.status).toBe(403);
|
expect(res.status).toBe(403);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("context is exclusive", async () => {
|
||||||
|
const app = createAuthApp();
|
||||||
|
await app.build();
|
||||||
|
const $fns = fns(app);
|
||||||
|
|
||||||
|
app.server.get("/get", auth(), async (c) => {
|
||||||
|
return c.json({
|
||||||
|
user: c.get("auth").user ?? null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
app.server.get("/wait", auth(), async (c) => {
|
||||||
|
await new Promise((r) => setTimeout(r, 20));
|
||||||
|
return c.json({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = await $fns.login(configs.users.normal);
|
||||||
|
const me = await $fns.me(data.token);
|
||||||
|
expect(me.user.email).toBe(configs.users.normal.email);
|
||||||
|
|
||||||
|
app.server.request("/wait", {
|
||||||
|
headers: { Authorization: `Bearer ${data.token}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
{
|
||||||
|
await new Promise((r) => setTimeout(r, 10));
|
||||||
|
const res = await app.server.request("/get");
|
||||||
|
const data = await res.json();
|
||||||
|
expect(data.user).toBe(null);
|
||||||
|
expect(await $fns.me()).toEqual({ user: null as any });
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
import { createApp } from "../../src";
|
||||||
|
import { Api } from "../../src/Api";
|
||||||
|
|
||||||
|
describe("integration config", () => {
|
||||||
|
it("should create an entity", async () => {
|
||||||
|
const app = createApp();
|
||||||
|
await app.build();
|
||||||
|
const api = new Api({
|
||||||
|
host: "http://localhost",
|
||||||
|
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",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(app.em.entities.map((e) => e.name)).toContain("posts");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,56 +1,97 @@
|
|||||||
import { describe, test } from "bun:test";
|
/// <reference types="@types/bun" />
|
||||||
import { Hono } from "hono";
|
|
||||||
import { Guard } from "../../src/auth";
|
|
||||||
import { EventManager } from "../../src/core/events";
|
|
||||||
import { EntityManager } from "../../src/data";
|
|
||||||
import { AppMedia } from "../../src/media/AppMedia";
|
|
||||||
import { MediaController } from "../../src/media/api/MediaController";
|
|
||||||
import { getDummyConnection } from "../helper";
|
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
|
import { createApp, registries } from "../../src";
|
||||||
|
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(() => {
|
||||||
* R2
|
registries.media.register("local", StorageLocalAdapter);
|
||||||
* value: ReadableStream | ArrayBuffer | ArrayBufferView | string | Blob | null,
|
});
|
||||||
* Node writefile
|
|
||||||
* data: string | NodeJS.ArrayBufferView | Iterable<string | NodeJS.ArrayBufferView> | AsyncIterable<string | NodeJS.ArrayBufferView> | Stream,
|
|
||||||
*/
|
|
||||||
const ALL_TESTS = !!process.env.ALL_TESTS;
|
|
||||||
describe.skipIf(ALL_TESTS)("MediaController", () => {
|
|
||||||
test("..", async () => {
|
|
||||||
const ctx: any = {
|
|
||||||
em: new EntityManager([], dummyConnection, []),
|
|
||||||
guard: new Guard(),
|
|
||||||
emgr: new EventManager(),
|
|
||||||
server: new Hono()
|
|
||||||
};
|
|
||||||
|
|
||||||
const media = new AppMedia(
|
const path = `${assetsPath}/image.png`;
|
||||||
// @ts-ignore
|
|
||||||
|
async function makeApp(mediaOverride: Partial<TAppMediaConfig> = {}) {
|
||||||
|
const app = createApp({
|
||||||
|
initialConfig: {
|
||||||
|
media: mergeObject(
|
||||||
{
|
{
|
||||||
enabled: true,
|
enabled: true,
|
||||||
adapter: {
|
adapter: {
|
||||||
type: "s3",
|
type: "local",
|
||||||
config: {
|
config: {
|
||||||
access_key: process.env.R2_ACCESS_KEY as string,
|
path: assetsTmpPath,
|
||||||
secret_access_key: process.env.R2_SECRET_ACCESS_KEY as string,
|
|
||||||
url: process.env.R2_URL as string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
ctx
|
},
|
||||||
);
|
},
|
||||||
await media.build();
|
mediaOverride,
|
||||||
const app = new MediaController(media).getController();
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const file = Bun.file(`${import.meta.dir}/adapters/icon.png`);
|
await app.build();
|
||||||
console.log("file", file);
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeName(ext: string) {
|
||||||
|
return randomString(10) + "." + ext;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(disableConsoleLog);
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
|
describe("MediaController", () => {
|
||||||
|
test.only("accepts direct", async () => {
|
||||||
|
const app = await makeApp();
|
||||||
|
|
||||||
|
const file = Bun.file(path);
|
||||||
|
const name = makeName("png");
|
||||||
|
const res = await app.server.request("/api/media/upload/" + name, {
|
||||||
|
method: "POST",
|
||||||
|
body: file,
|
||||||
|
});
|
||||||
|
const result = (await res.json()) as any;
|
||||||
|
console.log(result);
|
||||||
|
expect(result.name).toBe(name);
|
||||||
|
|
||||||
|
const destFile = Bun.file(assetsTmpPath + "/" + name);
|
||||||
|
expect(destFile.exists()).resolves.toBe(true);
|
||||||
|
await destFile.delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("accepts form data", async () => {
|
||||||
|
const app = await makeApp();
|
||||||
|
|
||||||
|
const file = Bun.file(path);
|
||||||
|
const name = makeName("png");
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.append("file", file);
|
form.append("file", file);
|
||||||
|
|
||||||
await app.request("/upload/test.png", {
|
const res = await app.server.request("/api/media/upload/" + name, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: file
|
body: form,
|
||||||
});
|
});
|
||||||
|
const result = (await res.json()) as any;
|
||||||
|
expect(result.name).toBe(name);
|
||||||
|
|
||||||
|
const destFile = Bun.file(assetsTmpPath + "/" + name);
|
||||||
|
expect(destFile.exists()).resolves.toBe(true);
|
||||||
|
await destFile.delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("limits body", async () => {
|
||||||
|
const app = await makeApp({ storage: { body_max_size: 1 } });
|
||||||
|
|
||||||
|
const file = await Bun.file(path);
|
||||||
|
const name = makeName("png");
|
||||||
|
const res = await app.server.request("/api/media/upload/" + name, {
|
||||||
|
method: "POST",
|
||||||
|
body: file,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(413);
|
||||||
|
expect(await Bun.file(assetsTmpPath + "/" + name).exists()).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { type FileBody, Storage, type StorageAdapter } from "../../src/media/storage/Storage";
|
import { type FileBody, Storage } from "../../src/media/storage/Storage";
|
||||||
import * as StorageEvents from "../../src/media/storage/events";
|
import * as StorageEvents from "../../src/media/storage/events";
|
||||||
|
import { StorageAdapter } from "media";
|
||||||
|
|
||||||
class TestAdapter implements StorageAdapter {
|
class TestAdapter extends StorageAdapter {
|
||||||
files: Record<string, FileBody> = {};
|
files: Record<string, FileBody> = {};
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
@@ -60,8 +61,8 @@ describe("Storage", async () => {
|
|||||||
|
|
||||||
test("uploads a file", async () => {
|
test("uploads a file", async () => {
|
||||||
const {
|
const {
|
||||||
meta: { type, size }
|
meta: { type, size },
|
||||||
} = await storage.uploadFile("hello", "world.txt");
|
} = await storage.uploadFile("hello" as any, "world.txt");
|
||||||
expect({ type, size }).toEqual({ type: "text/plain", size: 0 });
|
expect({ type, size }).toEqual({ type: "text/plain", size: 0 });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -71,6 +72,7 @@ describe("Storage", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("events were fired", async () => {
|
test("events were fired", async () => {
|
||||||
|
await storage.emgr.executeAsyncs();
|
||||||
expect(events.has(StorageEvents.FileUploadedEvent.slug)).toBeTrue();
|
expect(events.has(StorageEvents.FileUploadedEvent.slug)).toBeTrue();
|
||||||
expect(events.has(StorageEvents.FileDeletedEvent.slug)).toBeTrue();
|
expect(events.has(StorageEvents.FileDeletedEvent.slug)).toBeTrue();
|
||||||
// @todo: file access must be tested in controllers
|
// @todo: file access must be tested in controllers
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import * as assert from "node:assert/strict";
|
|
||||||
import { createWriteStream } from "node:fs";
|
|
||||||
import { test } from "node:test";
|
|
||||||
import { Miniflare } from "miniflare";
|
|
||||||
|
|
||||||
// https://github.com/nodejs/node/issues/44372#issuecomment-1736530480
|
|
||||||
console.log = async (message: any) => {
|
|
||||||
const tty = createWriteStream("/dev/tty");
|
|
||||||
const msg = typeof message === "string" ? message : JSON.stringify(message, null, 2);
|
|
||||||
return tty.write(`${msg}\n`);
|
|
||||||
};
|
|
||||||
|
|
||||||
test("what", async () => {
|
|
||||||
const mf = new Miniflare({
|
|
||||||
modules: true,
|
|
||||||
script: "export default { async fetch() { return new Response(null); } }",
|
|
||||||
r2Buckets: ["BUCKET"]
|
|
||||||
});
|
|
||||||
|
|
||||||
const bucket = await mf.getR2Bucket("BUCKET");
|
|
||||||
console.log(await bucket.put("count", "1"));
|
|
||||||
|
|
||||||
const object = await bucket.get("count");
|
|
||||||
if (object) {
|
|
||||||
/*const headers = new Headers();
|
|
||||||
object.writeHttpMetadata(headers);
|
|
||||||
headers.set("etag", object.httpEtag);*/
|
|
||||||
console.log("yo -->", await object.text());
|
|
||||||
|
|
||||||
assert.strictEqual(await object.text(), "1");
|
|
||||||
}
|
|
||||||
|
|
||||||
await mf.dispose();
|
|
||||||
});
|
|
||||||
@@ -1,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)).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 { describe, expect, test } from "bun:test";
|
||||||
import * as large from "../../src/media/storage/mime-types";
|
import * as large from "../../src/media/storage/mime-types";
|
||||||
import * as tiny from "../../src/media/storage/mime-types-tiny";
|
import * as tiny from "../../src/media/storage/mime-types-tiny";
|
||||||
|
import { getRandomizedFilename } from "../../src/media/utils";
|
||||||
|
|
||||||
describe("media/mime-types", () => {
|
describe("media/mime-types", () => {
|
||||||
test("tiny resolves", () => {
|
test("tiny resolves", () => {
|
||||||
const tests = [[".mp4", "video/mp4", ".jpg", "image/jpeg", ".zip", "application/zip"]];
|
const tests = [
|
||||||
|
[".mp4", "video/mp4", ".jpg", "image/jpeg", ".zip", "application/zip"],
|
||||||
|
] as const;
|
||||||
|
|
||||||
for (const [ext, mime] of tests) {
|
for (const [ext, mime] of tests) {
|
||||||
expect(tiny.guess(ext)).toBe(mime);
|
expect(tiny.guess(ext)).toBe(mime);
|
||||||
@@ -27,11 +30,76 @@ describe("media/mime-types", () => {
|
|||||||
exts,
|
exts,
|
||||||
ext,
|
ext,
|
||||||
expected: ex,
|
expected: ex,
|
||||||
actual: large.guessMimeType("." + ext)
|
actual: large.guessMimeType("." + ext),
|
||||||
});
|
});
|
||||||
throw new Error(`Failed for ${ext}`);
|
throw new Error(`Failed for ${ext}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("isMimeType", () => {
|
||||||
|
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", () => {
|
||||||
|
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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { AuthController } from "../../src/auth/api/AuthController";
|
|||||||
import { em, entity, make, text } from "../../src/data";
|
import { em, entity, make, text } from "../../src/data";
|
||||||
import { AppAuth, type ModuleBuildContext } from "../../src/modules";
|
import { AppAuth, type ModuleBuildContext } from "../../src/modules";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "../helper";
|
import { disableConsoleLog, enableConsoleLog } from "../helper";
|
||||||
|
// @ts-ignore
|
||||||
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
||||||
|
|
||||||
describe("AppAuth", () => {
|
describe("AppAuth", () => {
|
||||||
@@ -22,7 +23,7 @@ describe("AppAuth", () => {
|
|||||||
|
|
||||||
const config = auth.toJSON();
|
const config = auth.toJSON();
|
||||||
expect(config.jwt).toBeUndefined();
|
expect(config.jwt).toBeUndefined();
|
||||||
expect(config.strategies.password.config).toBeUndefined();
|
expect(config.strategies?.password?.config).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("enabling auth: generate secret", async () => {
|
test("enabling auth: generate secret", async () => {
|
||||||
@@ -42,11 +43,12 @@ describe("AppAuth", () => {
|
|||||||
const auth = new AppAuth(
|
const auth = new AppAuth(
|
||||||
{
|
{
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
// @ts-ignore
|
||||||
jwt: {
|
jwt: {
|
||||||
secret: "123456"
|
secret: "123456",
|
||||||
}
|
|
||||||
},
|
},
|
||||||
ctx
|
},
|
||||||
|
ctx,
|
||||||
);
|
);
|
||||||
|
|
||||||
await auth.build();
|
await auth.build();
|
||||||
@@ -63,19 +65,19 @@ describe("AppAuth", () => {
|
|||||||
const res = await app.request("/password/register", {
|
const res = await app.request("/password/register", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
email: "some@body.com",
|
email: "some@body.com",
|
||||||
password: "123456"
|
password: "123456",
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
enableConsoleLog();
|
enableConsoleLog();
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
|
|
||||||
const { data: users } = await ctx.em.repository("users").findMany();
|
const { data: users } = await ctx.em.repository("users").findMany();
|
||||||
expect(users.length).toBe(1);
|
expect(users.length).toBe(1);
|
||||||
expect(users[0].email).toBe("some@body.com");
|
expect(users[0]?.email).toBe("some@body.com");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -85,10 +87,10 @@ describe("AppAuth", () => {
|
|||||||
auth: {
|
auth: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
jwt: {
|
jwt: {
|
||||||
secret: "123456"
|
secret: "123456",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await app.build();
|
await app.build();
|
||||||
@@ -109,14 +111,14 @@ describe("AppAuth", () => {
|
|||||||
initialConfig: {
|
initialConfig: {
|
||||||
auth: {
|
auth: {
|
||||||
entity_name: "users",
|
entity_name: "users",
|
||||||
enabled: true
|
enabled: true,
|
||||||
},
|
},
|
||||||
data: em({
|
data: em({
|
||||||
users: entity("users", {
|
users: entity("users", {
|
||||||
additional: text()
|
additional: text(),
|
||||||
})
|
}),
|
||||||
}).toJSON()
|
}).toJSON(),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await app.build();
|
await app.build();
|
||||||
@@ -132,21 +134,21 @@ describe("AppAuth", () => {
|
|||||||
const app = createApp({
|
const app = createApp({
|
||||||
initialConfig: {
|
initialConfig: {
|
||||||
auth: {
|
auth: {
|
||||||
enabled: true
|
enabled: true,
|
||||||
},
|
},
|
||||||
data: em({
|
data: em({
|
||||||
users: entity("users", {
|
users: entity("users", {
|
||||||
strategy: text({
|
strategy: text({
|
||||||
fillable: true,
|
fillable: true,
|
||||||
hidden: false
|
hidden: false,
|
||||||
}),
|
}),
|
||||||
strategy_value: text({
|
strategy_value: text({
|
||||||
fillable: true,
|
fillable: true,
|
||||||
hidden: false
|
hidden: false,
|
||||||
})
|
}),
|
||||||
})
|
}),
|
||||||
}).toJSON()
|
}).toJSON(),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
await app.build();
|
await app.build();
|
||||||
|
|
||||||
@@ -157,7 +159,7 @@ describe("AppAuth", () => {
|
|||||||
const authField = make(name, _authFieldProto as any);
|
const authField = make(name, _authFieldProto as any);
|
||||||
const field = users.field(name)!;
|
const field = users.field(name)!;
|
||||||
for (const prop of props) {
|
for (const prop of props) {
|
||||||
expect(field.config[prop]).toBe(authField.config[prop]);
|
expect(field.config[prop]).toEqual(authField.config[prop]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { createApp, registries } from "../../src";
|
import { createApp, registries } from "../../src";
|
||||||
import { em, entity, text } from "../../src/data";
|
import { em, entity, text } from "../../src/data";
|
||||||
import { StorageLocalAdapter } from "../../src/media/storage/adapters/StorageLocalAdapter";
|
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||||
import { AppMedia } from "../../src/modules";
|
import { AppMedia } from "../../src/modules";
|
||||||
import { moduleTestSuite } from "./module-test-suite";
|
import { moduleTestSuite } from "./module-test-suite";
|
||||||
|
|
||||||
@@ -19,16 +19,16 @@ describe("AppMedia", () => {
|
|||||||
adapter: {
|
adapter: {
|
||||||
type: "local",
|
type: "local",
|
||||||
config: {
|
config: {
|
||||||
path: "./"
|
path: "./",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
data: em({
|
data: em({
|
||||||
media: entity("media", {
|
media: entity("media", {
|
||||||
additional: text()
|
additional: text(),
|
||||||
})
|
}),
|
||||||
}).toJSON()
|
}).toJSON(),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await app.build();
|
await app.build();
|
||||||
@@ -49,7 +49,7 @@ describe("AppMedia", () => {
|
|||||||
"modified_at",
|
"modified_at",
|
||||||
"reference",
|
"reference",
|
||||||
"entity_id",
|
"entity_id",
|
||||||
"metadata"
|
"metadata",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ describe("Module", async () => {
|
|||||||
prt = {
|
prt = {
|
||||||
ensureEntity: this.ensureEntity.bind(this),
|
ensureEntity: this.ensureEntity.bind(this),
|
||||||
ensureIndex: this.ensureIndex.bind(this),
|
ensureIndex: this.ensureIndex.bind(this),
|
||||||
ensureSchema: this.ensureSchema.bind(this)
|
ensureSchema: this.ensureSchema.bind(this),
|
||||||
};
|
};
|
||||||
|
|
||||||
get em() {
|
get em() {
|
||||||
@@ -60,7 +60,7 @@ describe("Module", async () => {
|
|||||||
Object.values(_em.entities),
|
Object.values(_em.entities),
|
||||||
new DummyConnection(),
|
new DummyConnection(),
|
||||||
_em.relations,
|
_em.relations,
|
||||||
_em.indices
|
_em.indices,
|
||||||
);
|
);
|
||||||
return new M({} as any, { em, flags: Module.ctx_flags } as any);
|
return new M({} as any, { em, flags: Module.ctx_flags } as any);
|
||||||
}
|
}
|
||||||
@@ -69,14 +69,14 @@ describe("Module", async () => {
|
|||||||
entities: _em.entities.map((e) => ({
|
entities: _em.entities.map((e) => ({
|
||||||
name: e.name,
|
name: e.name,
|
||||||
fields: e.fields.map((f) => f.name),
|
fields: e.fields.map((f) => f.name),
|
||||||
type: e.type
|
type: e.type,
|
||||||
})),
|
})),
|
||||||
indices: _em.indices.map((i) => ({
|
indices: _em.indices.map((i) => ({
|
||||||
name: i.name,
|
name: i.name,
|
||||||
entity: i.entity.name,
|
entity: i.entity.name,
|
||||||
fields: i.fields.map((f) => f.name),
|
fields: i.fields.map((f) => f.name),
|
||||||
unique: i.unique
|
unique: i.unique,
|
||||||
}))
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,15 +88,15 @@ describe("Module", async () => {
|
|||||||
|
|
||||||
expect(flat(make(initial).em)).toEqual({
|
expect(flat(make(initial).em)).toEqual({
|
||||||
entities: [],
|
entities: [],
|
||||||
indices: []
|
indices: [],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("init", () => {
|
test("init", () => {
|
||||||
const initial = em({
|
const initial = em({
|
||||||
users: entity("u", {
|
users: entity("u", {
|
||||||
name: text()
|
name: text(),
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const m = make(initial);
|
const m = make(initial);
|
||||||
@@ -107,18 +107,18 @@ describe("Module", async () => {
|
|||||||
{
|
{
|
||||||
name: "u",
|
name: "u",
|
||||||
fields: ["id", "name"],
|
fields: ["id", "name"],
|
||||||
type: "regular"
|
type: "regular",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
indices: []
|
indices: [],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ensure entity", () => {
|
test("ensure entity", () => {
|
||||||
const initial = em({
|
const initial = em({
|
||||||
users: entity("u", {
|
users: entity("u", {
|
||||||
name: text()
|
name: text(),
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const m = make(initial);
|
const m = make(initial);
|
||||||
@@ -127,17 +127,17 @@ describe("Module", async () => {
|
|||||||
{
|
{
|
||||||
name: "u",
|
name: "u",
|
||||||
fields: ["id", "name"],
|
fields: ["id", "name"],
|
||||||
type: "regular"
|
type: "regular",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
indices: []
|
indices: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
// this should add a new entity
|
// this should add a new entity
|
||||||
m.prt.ensureEntity(
|
m.prt.ensureEntity(
|
||||||
entity("p", {
|
entity("p", {
|
||||||
title: text()
|
title: text(),
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// this should only add the field "important"
|
// this should only add the field "important"
|
||||||
@@ -145,11 +145,11 @@ describe("Module", async () => {
|
|||||||
entity(
|
entity(
|
||||||
"u",
|
"u",
|
||||||
{
|
{
|
||||||
important: text()
|
important: text(),
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
"system"
|
"system",
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(m.ctx.flags.sync_required).toBe(true);
|
expect(m.ctx.flags.sync_required).toBe(true);
|
||||||
@@ -159,22 +159,22 @@ describe("Module", async () => {
|
|||||||
name: "u",
|
name: "u",
|
||||||
fields: ["id", "name", "important"],
|
fields: ["id", "name", "important"],
|
||||||
// ensured type must be present
|
// ensured type must be present
|
||||||
type: "system"
|
type: "system",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "p",
|
name: "p",
|
||||||
fields: ["id", "title"],
|
fields: ["id", "title"],
|
||||||
type: "regular"
|
type: "regular",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
indices: []
|
indices: [],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ensure index", () => {
|
test("ensure index", () => {
|
||||||
const users = entity("u", {
|
const users = entity("u", {
|
||||||
name: text(),
|
name: text(),
|
||||||
title: text()
|
title: text(),
|
||||||
});
|
});
|
||||||
const initial = em({ users }, ({ index }, { users }) => {
|
const initial = em({ users }, ({ index }, { users }) => {
|
||||||
index(users).on(["title"]);
|
index(users).on(["title"]);
|
||||||
@@ -189,23 +189,23 @@ describe("Module", async () => {
|
|||||||
{
|
{
|
||||||
name: "u",
|
name: "u",
|
||||||
fields: ["id", "name", "title"],
|
fields: ["id", "name", "title"],
|
||||||
type: "regular"
|
type: "regular",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
indices: [
|
indices: [
|
||||||
{
|
{
|
||||||
name: "idx_u_title",
|
name: "idx_u_title",
|
||||||
entity: "u",
|
entity: "u",
|
||||||
fields: ["title"],
|
fields: ["title"],
|
||||||
unique: false
|
unique: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "idx_u_name",
|
name: "idx_u_name",
|
||||||
entity: "u",
|
entity: "u",
|
||||||
fields: ["name"],
|
fields: ["name"],
|
||||||
unique: false
|
unique: false,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
||||||
import { stripMark } from "../../src/core/utils";
|
import { Type, disableConsoleLog, enableConsoleLog, stripMark } from "../../src/core/utils";
|
||||||
import { entity, text } from "../../src/data";
|
import { entity, text } from "../../src/data";
|
||||||
|
import { Module } from "../../src/modules/Module";
|
||||||
import { ModuleManager, getDefaultConfig } from "../../src/modules/ModuleManager";
|
import { ModuleManager, getDefaultConfig } from "../../src/modules/ModuleManager";
|
||||||
import { CURRENT_VERSION, TABLE_NAME } from "../../src/modules/migrations";
|
import { CURRENT_VERSION, TABLE_NAME } from "../../src/modules/migrations";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
@@ -38,11 +39,11 @@ describe("ModuleManager", async () => {
|
|||||||
basepath: "/api/data2",
|
basepath: "/api/data2",
|
||||||
entities: {
|
entities: {
|
||||||
test: entity("test", {
|
test: entity("test", {
|
||||||
content: text()
|
content: text(),
|
||||||
}).toJSON()
|
}).toJSON(),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
}) as any;
|
||||||
//const { version, ...json } = mm.toJSON() as any;
|
//const { version, ...json } = mm.toJSON() as any;
|
||||||
|
|
||||||
const c2 = getDummyConnection();
|
const c2 = getDummyConnection();
|
||||||
@@ -68,11 +69,11 @@ describe("ModuleManager", async () => {
|
|||||||
basepath: "/api/data2",
|
basepath: "/api/data2",
|
||||||
entities: {
|
entities: {
|
||||||
test: entity("test", {
|
test: entity("test", {
|
||||||
content: text()
|
content: text(),
|
||||||
}).toJSON()
|
}).toJSON(),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
} as any;
|
||||||
//const { version, ...json } = mm.toJSON() as any;
|
//const { version, ...json } = mm.toJSON() as any;
|
||||||
|
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
@@ -89,23 +90,20 @@ describe("ModuleManager", async () => {
|
|||||||
await mm2.build();
|
await mm2.build();
|
||||||
|
|
||||||
expect(stripMark(json)).toEqual(stripMark(mm2.configs()));
|
expect(stripMark(json)).toEqual(stripMark(mm2.configs()));
|
||||||
expect(mm2.configs().data.entities.test).toBeDefined();
|
expect(mm2.configs().data.entities?.test).toBeDefined();
|
||||||
expect(mm2.configs().data.entities.test.fields.content).toBeDefined();
|
expect(mm2.configs().data.entities?.test?.fields?.content).toBeDefined();
|
||||||
expect(mm2.get("data").toJSON().entities.test.fields.content).toBeDefined();
|
expect(mm2.get("data").toJSON().entities?.test?.fields?.content).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("s4: config given, table exists, version outdated, migrate", async () => {
|
test("s4: config given, table exists, version outdated, migrate", async () => {
|
||||||
const c = getDummyConnection();
|
const c = getDummyConnection();
|
||||||
const mm = new ModuleManager(c.dummyConnection);
|
const mm = new ModuleManager(c.dummyConnection);
|
||||||
await mm.build();
|
await mm.build();
|
||||||
const version = mm.version();
|
|
||||||
const json = mm.configs();
|
const json = mm.configs();
|
||||||
|
|
||||||
const c2 = getDummyConnection();
|
const c2 = getDummyConnection();
|
||||||
const db = c2.dummyConnection.kysely;
|
const db = c2.dummyConnection.kysely;
|
||||||
const mm2 = new ModuleManager(c2.dummyConnection, {
|
const mm2 = new ModuleManager(c2.dummyConnection);
|
||||||
initial: { version: version - 1, ...json }
|
|
||||||
});
|
|
||||||
await mm2.syncConfigTable();
|
await mm2.syncConfigTable();
|
||||||
|
|
||||||
await db
|
await db
|
||||||
@@ -128,7 +126,7 @@ describe("ModuleManager", async () => {
|
|||||||
const db = c2.dummyConnection.kysely;
|
const db = c2.dummyConnection.kysely;
|
||||||
|
|
||||||
const mm2 = new ModuleManager(c2.dummyConnection, {
|
const mm2 = new ModuleManager(c2.dummyConnection, {
|
||||||
initial: { version: version - 1, ...json }
|
initial: { version: version - 1, ...json },
|
||||||
});
|
});
|
||||||
await mm2.syncConfigTable();
|
await mm2.syncConfigTable();
|
||||||
await db
|
await db
|
||||||
@@ -156,8 +154,8 @@ describe("ModuleManager", async () => {
|
|||||||
...json,
|
...json,
|
||||||
data: {
|
data: {
|
||||||
...json.data,
|
...json.data,
|
||||||
basepath: "/api/data2"
|
basepath: "/api/data2",
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
await db
|
await db
|
||||||
.insertInto(TABLE_NAME)
|
.insertInto(TABLE_NAME)
|
||||||
@@ -170,14 +168,14 @@ describe("ModuleManager", async () => {
|
|||||||
expect(mm2.configs().data.basepath).toBe("/api/data2");
|
expect(mm2.configs().data.basepath).toBe("/api/data2");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("blank app, modify config", async () => {
|
/*test("blank app, modify config", async () => {
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
|
|
||||||
const mm = new ModuleManager(dummyConnection);
|
const mm = new ModuleManager(dummyConnection);
|
||||||
await mm.build();
|
await mm.build();
|
||||||
const configs = stripMark(mm.configs());
|
const configs = stripMark(mm.configs());
|
||||||
|
|
||||||
expect(mm.configs().server.admin.color_scheme).toBe("light");
|
expect(mm.configs().server.admin.color_scheme).toBeUndefined();
|
||||||
expect(() => mm.get("server").schema().patch("admin", { color_scheme: "violet" })).toThrow();
|
expect(() => mm.get("server").schema().patch("admin", { color_scheme: "violet" })).toThrow();
|
||||||
await mm.get("server").schema().patch("admin", { color_scheme: "dark" });
|
await mm.get("server").schema().patch("admin", { color_scheme: "dark" });
|
||||||
await mm.save();
|
await mm.save();
|
||||||
@@ -189,35 +187,37 @@ describe("ModuleManager", async () => {
|
|||||||
...configs.server,
|
...configs.server,
|
||||||
admin: {
|
admin: {
|
||||||
...configs.server.admin,
|
...configs.server.admin,
|
||||||
color_scheme: "dark"
|
color_scheme: "dark",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});*/
|
||||||
|
|
||||||
test("partial config given", async () => {
|
test("partial config given", async () => {
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
|
|
||||||
const partial = {
|
const partial = {
|
||||||
auth: {
|
auth: {
|
||||||
enabled: true
|
enabled: true,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
const mm = new ModuleManager(dummyConnection, {
|
const mm = new ModuleManager(dummyConnection, {
|
||||||
initial: partial
|
initial: partial,
|
||||||
});
|
});
|
||||||
await mm.build();
|
await mm.build();
|
||||||
|
|
||||||
expect(mm.version()).toBe(CURRENT_VERSION);
|
expect(mm.version()).toBe(CURRENT_VERSION);
|
||||||
expect(mm.built()).toBe(true);
|
expect(mm.built()).toBe(true);
|
||||||
expect(mm.configs().auth.enabled).toBe(true);
|
expect(mm.configs().auth.enabled).toBe(true);
|
||||||
expect(mm.configs().data.entities.users).toBeDefined();
|
expect(mm.configs().data.entities?.users).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("partial config given, but db version exists", async () => {
|
test("partial config given, but db version exists", async () => {
|
||||||
const c = getDummyConnection();
|
const c = getDummyConnection();
|
||||||
const mm = new ModuleManager(c.dummyConnection);
|
const mm = new ModuleManager(c.dummyConnection);
|
||||||
await mm.build();
|
await mm.build();
|
||||||
|
console.log("==".repeat(30));
|
||||||
|
console.log("");
|
||||||
const json = mm.configs();
|
const json = mm.configs();
|
||||||
|
|
||||||
const c2 = getDummyConnection();
|
const c2 = getDummyConnection();
|
||||||
@@ -226,9 +226,9 @@ describe("ModuleManager", async () => {
|
|||||||
const mm2 = new ModuleManager(c2.dummyConnection, {
|
const mm2 = new ModuleManager(c2.dummyConnection, {
|
||||||
initial: {
|
initial: {
|
||||||
auth: {
|
auth: {
|
||||||
basepath: "/shouldnt/take/this"
|
basepath: "/shouldnt/take/this",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
await mm2.syncConfigTable();
|
await mm2.syncConfigTable();
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -236,15 +236,15 @@ describe("ModuleManager", async () => {
|
|||||||
auth: {
|
auth: {
|
||||||
...json.auth,
|
...json.auth,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
basepath: "/api/auth2"
|
basepath: "/api/auth2",
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
await db
|
await db
|
||||||
.insertInto(TABLE_NAME)
|
.insertInto(TABLE_NAME)
|
||||||
.values({
|
.values({
|
||||||
type: "config",
|
type: "config",
|
||||||
json: JSON.stringify(payload),
|
json: JSON.stringify(payload),
|
||||||
version: CURRENT_VERSION
|
version: CURRENT_VERSION,
|
||||||
})
|
})
|
||||||
.execute();
|
.execute();
|
||||||
await mm2.build();
|
await mm2.build();
|
||||||
@@ -252,4 +252,132 @@ describe("ModuleManager", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// @todo: add tests for migrations (check "backup" and new version)
|
// @todo: add tests for migrations (check "backup" and new version)
|
||||||
|
|
||||||
|
describe("revert", async () => {
|
||||||
|
const failingModuleSchema = Type.Object({
|
||||||
|
value: Type.Optional(Type.Number()),
|
||||||
|
});
|
||||||
|
class FailingModule extends Module<typeof failingModuleSchema> {
|
||||||
|
getSchema() {
|
||||||
|
return failingModuleSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
override async build() {
|
||||||
|
//console.log("building FailingModule", this.config);
|
||||||
|
if (this.config.value && this.config.value < 0) {
|
||||||
|
throw new Error("value must be positive, given: " + this.config.value);
|
||||||
|
}
|
||||||
|
this.setBuilt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class TestModuleManager extends ModuleManager {
|
||||||
|
constructor(...args: ConstructorParameters<typeof ModuleManager>) {
|
||||||
|
super(...args);
|
||||||
|
const [, options] = args;
|
||||||
|
// @ts-ignore
|
||||||
|
const initial = options?.initial?.failing ?? {};
|
||||||
|
this.modules["failing"] = new FailingModule(initial, this.ctx());
|
||||||
|
this.modules["failing"].setListener(async (c) => {
|
||||||
|
// @ts-ignore
|
||||||
|
await this.onModuleConfigUpdated("failing", c);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => disableConsoleLog(["log", "warn", "error"]));
|
||||||
|
afterEach(enableConsoleLog);
|
||||||
|
|
||||||
|
test("it builds", async () => {
|
||||||
|
const { dummyConnection } = getDummyConnection();
|
||||||
|
const mm = new TestModuleManager(dummyConnection);
|
||||||
|
expect(mm).toBeDefined();
|
||||||
|
await mm.build();
|
||||||
|
expect(mm.toJSON()).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it accepts config", async () => {
|
||||||
|
const { dummyConnection } = getDummyConnection();
|
||||||
|
const mm = new TestModuleManager(dummyConnection, {
|
||||||
|
initial: {
|
||||||
|
// @ts-ignore
|
||||||
|
failing: { value: 2 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await mm.build();
|
||||||
|
expect(mm.configs()["failing"].value).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it crashes on invalid", async () => {
|
||||||
|
const { dummyConnection } = getDummyConnection();
|
||||||
|
const mm = new TestModuleManager(dummyConnection, {
|
||||||
|
initial: {
|
||||||
|
// @ts-ignore
|
||||||
|
failing: { value: -1 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(mm.build()).rejects.toThrow(/value must be positive/);
|
||||||
|
expect(mm.configs()["failing"].value).toBe(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it correctly accepts valid", async () => {
|
||||||
|
const mockOnUpdated = mock(() => null);
|
||||||
|
const { dummyConnection } = getDummyConnection();
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it reverts on safe mutate", async () => {
|
||||||
|
const mockOnUpdated = mock(() => null);
|
||||||
|
const { dummyConnection } = getDummyConnection();
|
||||||
|
const mm = new TestModuleManager(dummyConnection, {
|
||||||
|
initial: {
|
||||||
|
// @ts-ignore
|
||||||
|
failing: { value: 1 },
|
||||||
|
},
|
||||||
|
onUpdated: async () => {
|
||||||
|
mockOnUpdated();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await mm.build();
|
||||||
|
expect(mm.configs()["failing"].value).toBe(1);
|
||||||
|
|
||||||
|
// now safe mutate
|
||||||
|
// @ts-ignore
|
||||||
|
expect(mm.mutateConfigSafe("failing").set({ value: -2 })).rejects.toThrow(
|
||||||
|
/value must be positive/,
|
||||||
|
);
|
||||||
|
expect(mm.configs()["failing"].value).toBe(1);
|
||||||
|
expect(mockOnUpdated).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it only accepts schema mutating methods", async () => {
|
||||||
|
const { dummyConnection } = getDummyConnection();
|
||||||
|
const mm = new TestModuleManager(dummyConnection);
|
||||||
|
await mm.build();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const f = mm.mutateConfigSafe("failing");
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
expect(() => f.has("value")).toThrow();
|
||||||
|
// @ts-expect-error
|
||||||
|
expect(() => f.bypass()).toThrow();
|
||||||
|
// @ts-expect-error
|
||||||
|
expect(() => f.clone()).toThrow();
|
||||||
|
// @ts-expect-error
|
||||||
|
expect(() => f.get()).toThrow();
|
||||||
|
// @ts-expect-error
|
||||||
|
expect(() => f.default()).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildCon
|
|||||||
guard: new Guard(),
|
guard: new Guard(),
|
||||||
flags: Module.ctx_flags,
|
flags: Module.ctx_flags,
|
||||||
logger: new DebugLogger(false),
|
logger: new DebugLogger(false),
|
||||||
...overrides
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ describe("json form", () => {
|
|||||||
["0", { type: "boolean" }, false],
|
["0", { type: "boolean" }, false],
|
||||||
["on", { type: "boolean" }, true],
|
["on", { type: "boolean" }, true],
|
||||||
["off", { type: "boolean" }, false],
|
["off", { type: "boolean" }, false],
|
||||||
["null", { type: "null" }, null]
|
["null", { type: "null" }, null],
|
||||||
] satisfies [string, Exclude<JSONSchema, boolean>, any][];
|
] satisfies [string, Exclude<JSONSchema, boolean>, any][];
|
||||||
|
|
||||||
for (const [input, schema, output] of examples) {
|
for (const [input, schema, output] of examples) {
|
||||||
@@ -35,7 +35,7 @@ describe("json form", () => {
|
|||||||
["array", "array", true],
|
["array", "array", true],
|
||||||
["object", "array", false],
|
["object", "array", false],
|
||||||
[["string", "number"], "number", true],
|
[["string", "number"], "number", true],
|
||||||
["number", ["string", "number"], true]
|
["number", ["string", "number"], true],
|
||||||
] satisfies [IsTypeType, IsTypeType, boolean][];
|
] satisfies [IsTypeType, IsTypeType, boolean][];
|
||||||
|
|
||||||
for (const [type, schemaType, output] of examples) {
|
for (const [type, schemaType, output] of examples) {
|
||||||
@@ -48,7 +48,7 @@ describe("json form", () => {
|
|||||||
["#/nested/property/0/name", "#/nested/property/0"],
|
["#/nested/property/0/name", "#/nested/property/0"],
|
||||||
["#/nested/property/0", "#/nested/property"],
|
["#/nested/property/0", "#/nested/property"],
|
||||||
["#/nested/property", "#/nested"],
|
["#/nested/property", "#/nested"],
|
||||||
["#/nested", "#"]
|
["#/nested", "#"],
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const [input, output] of examples) {
|
for (const [input, output] of examples) {
|
||||||
@@ -61,16 +61,16 @@ describe("json form", () => {
|
|||||||
[
|
[
|
||||||
"#/description",
|
"#/description",
|
||||||
{ type: "object", properties: { description: { type: "string" } } },
|
{ type: "object", properties: { description: { type: "string" } } },
|
||||||
false
|
false,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"#/description",
|
"#/description",
|
||||||
{
|
{
|
||||||
type: "object",
|
type: "object",
|
||||||
required: ["description"],
|
required: ["description"],
|
||||||
properties: { description: { type: "string" } }
|
properties: { description: { type: "string" } },
|
||||||
},
|
},
|
||||||
true
|
true,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"#/nested/property",
|
"#/nested/property",
|
||||||
@@ -79,11 +79,11 @@ describe("json form", () => {
|
|||||||
properties: {
|
properties: {
|
||||||
nested: {
|
nested: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: { property: { type: "string" } }
|
properties: { property: { type: "string" } },
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
false
|
},
|
||||||
|
},
|
||||||
|
false,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"#/nested/property",
|
"#/nested/property",
|
||||||
@@ -93,12 +93,12 @@ describe("json form", () => {
|
|||||||
nested: {
|
nested: {
|
||||||
type: "object",
|
type: "object",
|
||||||
required: ["property"],
|
required: ["property"],
|
||||||
properties: { property: { type: "string" } }
|
properties: { property: { type: "string" } },
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
true
|
},
|
||||||
]
|
},
|
||||||
|
true,
|
||||||
|
],
|
||||||
] satisfies [string, Exclude<JSONSchema, boolean>, boolean][];
|
] satisfies [string, Exclude<JSONSchema, boolean>, boolean][];
|
||||||
|
|
||||||
for (const [pointer, schema, output] of examples) {
|
for (const [pointer, schema, output] of examples) {
|
||||||
@@ -113,7 +113,7 @@ describe("json form", () => {
|
|||||||
["tags", "0", "0.tags"],
|
["tags", "0", "0.tags"],
|
||||||
["tags", 0, "0.tags"],
|
["tags", 0, "0.tags"],
|
||||||
["nested.property", "prefix", "prefix.nested.property"],
|
["nested.property", "prefix", "prefix.nested.property"],
|
||||||
["nested.property", "", "nested.property"]
|
["nested.property", "", "nested.property"],
|
||||||
] satisfies [string, any, string][];
|
] satisfies [string, any, string][];
|
||||||
|
|
||||||
for (const [path, prefix, output] of examples) {
|
for (const [path, prefix, output] of examples) {
|
||||||
@@ -128,7 +128,7 @@ describe("json form", () => {
|
|||||||
["tags", "0", "tags.0"],
|
["tags", "0", "tags.0"],
|
||||||
["tags", 0, "tags.0"],
|
["tags", 0, "tags.0"],
|
||||||
["nested.property", "suffix", "nested.property.suffix"],
|
["nested.property", "suffix", "nested.property.suffix"],
|
||||||
["nested.property", "", "nested.property"]
|
["nested.property", "", "nested.property"],
|
||||||
] satisfies [string, any, string][];
|
] satisfies [string, any, string][];
|
||||||
|
|
||||||
for (const [path, suffix, output] of examples) {
|
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:"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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()]);
|
|
||||||
}
|
|
||||||
+111
-53
@@ -27,9 +27,9 @@ function buildTypes() {
|
|||||||
onExit: () => {
|
onExit: () => {
|
||||||
console.log("Types aliased");
|
console.log("Types aliased");
|
||||||
types_running = false;
|
types_running = false;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,40 +46,63 @@ if (types && !watch) {
|
|||||||
buildTypes();
|
buildTypes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function banner(title: string) {
|
||||||
|
console.log("");
|
||||||
|
console.log("=".repeat(40));
|
||||||
|
console.log(title.toUpperCase());
|
||||||
|
console.log("-".repeat(40));
|
||||||
|
}
|
||||||
|
|
||||||
|
// collection of always-external packages
|
||||||
|
const external = ["bun:test", "node:test", "node:assert/strict", "@libsql/client"] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Building backend and general API
|
* Building backend and general API
|
||||||
*/
|
*/
|
||||||
await tsup.build({
|
async function buildApi() {
|
||||||
|
banner("Building API");
|
||||||
|
await tsup.build({
|
||||||
minify,
|
minify,
|
||||||
sourcemap,
|
sourcemap,
|
||||||
watch,
|
watch,
|
||||||
entry: ["src/index.ts", "src/data/index.ts", "src/core/index.ts", "src/core/utils/index.ts"],
|
entry: [
|
||||||
|
"src/index.ts",
|
||||||
|
"src/core/index.ts",
|
||||||
|
"src/core/utils/index.ts",
|
||||||
|
"src/data/index.ts",
|
||||||
|
"src/media/index.ts",
|
||||||
|
],
|
||||||
outDir: "dist",
|
outDir: "dist",
|
||||||
external: ["bun:test", "@libsql/client"],
|
external: [...external],
|
||||||
metafile: true,
|
metafile: true,
|
||||||
platform: "browser",
|
platform: "browser",
|
||||||
format: ["esm"],
|
format: ["esm"],
|
||||||
splitting: false,
|
splitting: false,
|
||||||
treeshake: true,
|
treeshake: true,
|
||||||
loader: {
|
loader: {
|
||||||
".svg": "dataurl"
|
".svg": "dataurl",
|
||||||
},
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
delayTypes();
|
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
|
* Building UI for direct imports
|
||||||
*/
|
*/
|
||||||
await tsup.build({
|
async function buildUi() {
|
||||||
|
const base = {
|
||||||
minify,
|
minify,
|
||||||
sourcemap,
|
sourcemap,
|
||||||
watch,
|
watch,
|
||||||
entry: ["src/ui/index.ts", "src/ui/client/index.ts", "src/ui/main.css", "src/ui/styles.css"],
|
|
||||||
outDir: "dist/ui",
|
|
||||||
external: [
|
external: [
|
||||||
"bun:test",
|
...external,
|
||||||
"react",
|
"react",
|
||||||
"react-dom",
|
"react-dom",
|
||||||
"react/jsx-runtime",
|
"react/jsx-runtime",
|
||||||
@@ -87,7 +110,7 @@ await tsup.build({
|
|||||||
"use-sync-external-store",
|
"use-sync-external-store",
|
||||||
/codemirror/,
|
/codemirror/,
|
||||||
"@xyflow/react",
|
"@xyflow/react",
|
||||||
"@mantine/core"
|
"@mantine/core",
|
||||||
],
|
],
|
||||||
metafile: true,
|
metafile: true,
|
||||||
platform: "browser",
|
platform: "browser",
|
||||||
@@ -96,22 +119,44 @@ await tsup.build({
|
|||||||
bundle: true,
|
bundle: true,
|
||||||
treeshake: true,
|
treeshake: true,
|
||||||
loader: {
|
loader: {
|
||||||
".svg": "dataurl"
|
".svg": "dataurl",
|
||||||
},
|
},
|
||||||
esbuildOptions: (options) => {
|
esbuildOptions: (options) => {
|
||||||
options.logLevel = "silent";
|
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 () => {
|
onSuccess: async () => {
|
||||||
|
await rewriteClient("./dist/ui/index.js");
|
||||||
delayTypes();
|
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();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Building UI Elements
|
* Building UI Elements
|
||||||
* - tailwind-merge is mocked, no exclude
|
* - tailwind-merge is mocked, no exclude
|
||||||
* - ui/client is external, and after built replaced with "bknd/client"
|
* - ui/client is external, and after built replaced with "bknd/client"
|
||||||
*/
|
*/
|
||||||
await tsup.build({
|
async function buildUiElements() {
|
||||||
|
banner("Building UI Elements");
|
||||||
|
await tsup.build({
|
||||||
minify,
|
minify,
|
||||||
sourcemap,
|
sourcemap,
|
||||||
watch,
|
watch,
|
||||||
@@ -123,7 +168,7 @@ await tsup.build({
|
|||||||
"react-dom",
|
"react-dom",
|
||||||
"react/jsx-runtime",
|
"react/jsx-runtime",
|
||||||
"react/jsx-dev-runtime",
|
"react/jsx-dev-runtime",
|
||||||
"use-sync-external-store"
|
"use-sync-external-store",
|
||||||
],
|
],
|
||||||
metafile: true,
|
metafile: true,
|
||||||
platform: "browser",
|
platform: "browser",
|
||||||
@@ -132,28 +177,25 @@ await tsup.build({
|
|||||||
bundle: true,
|
bundle: true,
|
||||||
treeshake: true,
|
treeshake: true,
|
||||||
loader: {
|
loader: {
|
||||||
".svg": "dataurl"
|
".svg": "dataurl",
|
||||||
},
|
},
|
||||||
esbuildOptions: (options) => {
|
esbuildOptions: (options) => {
|
||||||
options.alias = {
|
options.alias = {
|
||||||
// not important for elements, mock to reduce bundle
|
// 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 () => {
|
onSuccess: async () => {
|
||||||
// manually replace ui/client with bknd/client
|
await rewriteClient("./dist/ui/elements/index.js");
|
||||||
const path = "./dist/ui/elements/index.js";
|
|
||||||
const bundle = await Bun.file(path).text();
|
|
||||||
await Bun.write(path, bundle.replaceAll("ui/client", "bknd/client"));
|
|
||||||
|
|
||||||
delayTypes();
|
delayTypes();
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Building adapters
|
* Building adapters
|
||||||
*/
|
*/
|
||||||
function baseConfig(adapter: string): tsup.Options {
|
function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsup.Options {
|
||||||
return {
|
return {
|
||||||
minify,
|
minify,
|
||||||
sourcemap,
|
sourcemap,
|
||||||
@@ -162,47 +204,63 @@ function baseConfig(adapter: string): tsup.Options {
|
|||||||
format: ["esm"],
|
format: ["esm"],
|
||||||
platform: "neutral",
|
platform: "neutral",
|
||||||
outDir: `dist/adapter/${adapter}`,
|
outDir: `dist/adapter/${adapter}`,
|
||||||
|
metafile: true,
|
||||||
|
splitting: false,
|
||||||
|
onSuccess: async () => {
|
||||||
|
delayTypes();
|
||||||
|
},
|
||||||
|
...overrides,
|
||||||
define: {
|
define: {
|
||||||
__isDev: "0"
|
__isDev: "0",
|
||||||
|
...overrides.define,
|
||||||
},
|
},
|
||||||
external: [
|
external: [
|
||||||
/^cloudflare*/,
|
/^cloudflare*/,
|
||||||
/^@?(hono|libsql).*?/,
|
/^@?(hono|libsql).*?/,
|
||||||
/^(bknd|react|next|node).*?/,
|
/^(bknd|react|next|node).*?/,
|
||||||
/.*\.(html)$/
|
/.*\.(html)$/,
|
||||||
|
...(Array.isArray(overrides.external) ? overrides.external : []),
|
||||||
],
|
],
|
||||||
metafile: true,
|
|
||||||
splitting: false,
|
|
||||||
onSuccess: async () => {
|
|
||||||
delayTypes();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// base adapter handles
|
async function buildAdapters() {
|
||||||
await tsup.build({
|
banner("Building Adapters");
|
||||||
|
// base adapter handles
|
||||||
|
await tsup.build({
|
||||||
...baseConfig(""),
|
...baseConfig(""),
|
||||||
entry: ["src/adapter/index.ts"],
|
entry: ["src/adapter/index.ts"],
|
||||||
outDir: "dist/adapter"
|
outDir: "dist/adapter",
|
||||||
});
|
});
|
||||||
|
|
||||||
// specific adatpers
|
// specific adatpers
|
||||||
await tsup.build(baseConfig("remix"));
|
await tsup.build(baseConfig("react-router"));
|
||||||
await tsup.build(baseConfig("bun"));
|
await tsup.build(baseConfig("bun"));
|
||||||
await tsup.build(baseConfig("astro"));
|
await tsup.build(baseConfig("astro"));
|
||||||
await tsup.build(baseConfig("cloudflare"));
|
await tsup.build(baseConfig("aws"));
|
||||||
|
await tsup.build(
|
||||||
|
baseConfig("cloudflare", {
|
||||||
|
external: [/^kysely/],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
await tsup.build({
|
await tsup.build({
|
||||||
...baseConfig("vite"),
|
...baseConfig("vite"),
|
||||||
platform: "node"
|
platform: "node",
|
||||||
});
|
});
|
||||||
|
|
||||||
await tsup.build({
|
await tsup.build({
|
||||||
...baseConfig("nextjs"),
|
...baseConfig("nextjs"),
|
||||||
platform: "node"
|
platform: "node",
|
||||||
});
|
});
|
||||||
|
|
||||||
await tsup.build({
|
await tsup.build({
|
||||||
...baseConfig("node"),
|
...baseConfig("node"),
|
||||||
platform: "node"
|
platform: "node",
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await buildApi();
|
||||||
|
await buildUi();
|
||||||
|
await buildUiElements();
|
||||||
|
await buildAdapters();
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
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"),
|
||||||
|
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;
|
||||||
|
|
||||||
|
for (const [name, config] of Object.entries(adapters)) {
|
||||||
|
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);
|
||||||
|
|
||||||
|
await $`TEST_URL=${data} TEST_ADAPTER=${name} bun run test:e2e`;
|
||||||
|
console.log("DONE!");
|
||||||
|
|
||||||
|
while (!proc.killed) {
|
||||||
|
proc.kill("SIGINT");
|
||||||
|
await Bun.sleep(250);
|
||||||
|
console.log("Waiting for process to exit...");
|
||||||
|
}
|
||||||
|
//process.exit(0);
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 265 KiB |
@@ -0,0 +1,25 @@
|
|||||||
|
// @ts-check
|
||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
import { testIds } from "../src/ui/lib/config";
|
||||||
|
|
||||||
|
import { getAdapterConfig } from "./inc/adapters";
|
||||||
|
const config = getAdapterConfig();
|
||||||
|
|
||||||
|
test("start page has expected title", async ({ page }) => {
|
||||||
|
await page.goto(config.base_path);
|
||||||
|
await expect(page).toHaveTitle(/BKND/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("start page has expected heading", async ({ page }) => {
|
||||||
|
await page.goto(config.base_path);
|
||||||
|
|
||||||
|
// Example of checking if a heading with "No entity selected" exists and is visible
|
||||||
|
const heading = page.getByRole("heading", { name: /No entity selected/i });
|
||||||
|
await expect(heading).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("modal opens on button click", async ({ page }) => {
|
||||||
|
await page.goto(config.base_path);
|
||||||
|
await page.getByTestId(testIds.data.btnCreateEntity).click();
|
||||||
|
await expect(page.getByRole("dialog")).toBeVisible();
|
||||||
|
});
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
const adapter = process.env.TEST_ADAPTER;
|
||||||
|
|
||||||
|
const default_config = {
|
||||||
|
media_adapter: "local",
|
||||||
|
base_path: "",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const configs = {
|
||||||
|
cloudflare: {
|
||||||
|
media_adapter: "r2",
|
||||||
|
},
|
||||||
|
"react-router": {
|
||||||
|
base_path: "/admin",
|
||||||
|
},
|
||||||
|
nextjs: {
|
||||||
|
base_path: "/admin",
|
||||||
|
},
|
||||||
|
astro: {
|
||||||
|
base_path: "/admin",
|
||||||
|
},
|
||||||
|
node: {
|
||||||
|
base_path: "",
|
||||||
|
},
|
||||||
|
bun: {
|
||||||
|
base_path: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getAdapterConfig(): typeof default_config {
|
||||||
|
if (adapter) {
|
||||||
|
if (!configs[adapter]) {
|
||||||
|
console.warn(
|
||||||
|
`Adapter "${adapter}" not found. Available adapters: ${Object.keys(configs).join(", ")}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
...default_config,
|
||||||
|
...configs[adapter],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return default_config;
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// @ts-check
|
||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
import { testIds } from "../src/ui/lib/config";
|
||||||
|
import type { SchemaResponse } from "../src/modules/server/SystemController";
|
||||||
|
import { getAdapterConfig } from "./inc/adapters";
|
||||||
|
|
||||||
|
// Annotate entire file as serial.
|
||||||
|
test.describe.configure({ mode: "serial" });
|
||||||
|
|
||||||
|
const config = getAdapterConfig();
|
||||||
|
|
||||||
|
test("can enable media", async ({ page }) => {
|
||||||
|
await page.goto(`${config.base_path}/media/settings`);
|
||||||
|
|
||||||
|
// enable
|
||||||
|
const enableToggle = page.locator("css=button#enabled");
|
||||||
|
if ((await enableToggle.getAttribute("aria-checked")) !== "true") {
|
||||||
|
await expect(enableToggle).toBeVisible();
|
||||||
|
await enableToggle.click();
|
||||||
|
await expect(enableToggle).toHaveAttribute("aria-checked", "true");
|
||||||
|
|
||||||
|
// select local
|
||||||
|
const adapterChoice = page.locator(`css=button#adapter-${config.media_adapter}`);
|
||||||
|
await expect(adapterChoice).toBeVisible();
|
||||||
|
await adapterChoice.click();
|
||||||
|
|
||||||
|
// save
|
||||||
|
const saveBtn = page.getByRole("button", { name: /Update/i });
|
||||||
|
await expect(saveBtn).toBeVisible();
|
||||||
|
|
||||||
|
// intercept network request, wait for it to finish and get the response
|
||||||
|
const [request] = await Promise.all([
|
||||||
|
page.waitForRequest((request) => request.url().includes("api/system/schema")),
|
||||||
|
saveBtn.click(),
|
||||||
|
]);
|
||||||
|
const response = await request.response();
|
||||||
|
expect(response?.status(), "fresh config 200").toBe(200);
|
||||||
|
const body = (await response?.json()) as SchemaResponse;
|
||||||
|
expect(body.config.media.enabled, "media is enabled").toBe(true);
|
||||||
|
expect(body.config.media.adapter?.type, "correct adapter").toBe(config.media_adapter);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can upload a file", async ({ page }) => {
|
||||||
|
await page.goto(`${config.base_path}/media`);
|
||||||
|
// check any text to contain "Upload files"
|
||||||
|
await expect(page.getByText(/Upload files/i)).toBeVisible();
|
||||||
|
|
||||||
|
// upload a file from disk
|
||||||
|
// Start waiting for file chooser before clicking. Note no await.
|
||||||
|
const fileChooserPromise = page.waitForEvent("filechooser");
|
||||||
|
await page.getByText("Upload file").click();
|
||||||
|
const fileChooser = await fileChooserPromise;
|
||||||
|
await fileChooser.setFiles("./e2e/assets/image.jpg");
|
||||||
|
});
|
||||||
@@ -5,8 +5,8 @@ export const entryOutputMeta = (
|
|||||||
outputs: {
|
outputs: {
|
||||||
output: string;
|
output: string;
|
||||||
meta: Metafile["outputs"][string];
|
meta: Metafile["outputs"][string];
|
||||||
}[]
|
}[],
|
||||||
) => void | Promise<void>
|
) => void | Promise<void>,
|
||||||
): Plugin => ({
|
): Plugin => ({
|
||||||
name: "report-entry-output-plugin",
|
name: "report-entry-output-plugin",
|
||||||
setup(build) {
|
setup(build) {
|
||||||
@@ -29,5 +29,5 @@ export const entryOutputMeta = (
|
|||||||
await onComplete?.(outputs);
|
await onComplete?.(outputs);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
+112
-72
@@ -3,8 +3,8 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"bin": "./dist/cli/index.js",
|
"bin": "./dist/cli/index.js",
|
||||||
"version": "0.7.1",
|
"version": "0.11.0-rc.2",
|
||||||
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
|
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
|
||||||
"homepage": "https://bknd.io",
|
"homepage": "https://bknd.io",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -15,92 +15,121 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"test": "ALL_TESTS=1 bun test --bail",
|
|
||||||
"test:coverage": "ALL_TESTS=1 bun test --bail --coverage",
|
|
||||||
"build": "NODE_ENV=production bun run build.ts --minify --types",
|
"build": "NODE_ENV=production bun run build.ts --minify --types",
|
||||||
"build:all": "rm -rf dist && bun run build:static && NODE_ENV=production bun run build.ts --minify --types --clean && bun run build:cli",
|
"build:all": "rm -rf dist && bun run build:static && NODE_ENV=production bun run build.ts --minify --types --clean && bun run build:cli",
|
||||||
"build:cli": "bun build src/cli/index.ts --target node --outdir dist/cli --minify",
|
"build:ci": "mkdir -p dist/static/.vite && echo '{}' > dist/static/.vite/manifest.json && NODE_ENV=production bun run build.ts",
|
||||||
|
"build:cli": "bun build src/cli/index.ts --target node --outdir dist/cli --env PUBLIC_* --minify",
|
||||||
"build:static": "vite build",
|
"build:static": "vite build",
|
||||||
"watch": "bun run build.ts --types --watch",
|
"watch": "bun run build.ts --types --watch",
|
||||||
"types": "bun tsc --noEmit",
|
"types": "bun tsc -p tsconfig.build.json --noEmit",
|
||||||
"clean:types": "find ./dist -name '*.d.ts' -delete && rm -f ./dist/tsconfig.tsbuildinfo",
|
"clean:types": "find ./dist -name '*.d.ts' -delete && rm -f ./dist/tsconfig.tsbuildinfo",
|
||||||
"build:types": "tsc --emitDeclarationOnly && tsc-alias",
|
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly && tsc-alias",
|
||||||
"updater": "bun x npm-check-updates -ui",
|
"updater": "bun x npm-check-updates -ui",
|
||||||
"cli": "LOCAL=1 bun src/cli/index.ts",
|
"cli": "LOCAL=1 bun src/cli/index.ts",
|
||||||
"prepublishOnly": "bun run types && bun run test && bun run build:all && cp ../README.md ./",
|
"prepublishOnly": "bun run types && bun run test && bun run test:node && bun run build:all && cp ../README.md ./",
|
||||||
"postpublish": "rm -f README.md"
|
"postpublish": "rm -f README.md",
|
||||||
|
"test": "ALL_TESTS=1 bun test --bail",
|
||||||
|
"test:all": "bun run test && bun run test:node",
|
||||||
|
"test:bun": "ALL_TESTS=1 bun test --bail",
|
||||||
|
"test:node": "tsx --test $(find . -type f -name '*.native-spec.ts')",
|
||||||
|
"test:adapters": "bun test src/adapter/**/*.adapter.spec.ts --bail",
|
||||||
|
"test:coverage": "ALL_TESTS=1 bun test --bail --coverage",
|
||||||
|
"test:vitest": "vitest run",
|
||||||
|
"test:vitest:watch": "vitest",
|
||||||
|
"test:vitest:coverage": "vitest run --coverage",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:ui": "playwright test --ui",
|
||||||
|
"test:e2e:debug": "playwright test --debug",
|
||||||
|
"test:e2e:report": "playwright show-report"
|
||||||
},
|
},
|
||||||
"license": "FSL-1.1-MIT",
|
"license": "FSL-1.1-MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cfworker/json-schema": "^2.0.1",
|
"@cfworker/json-schema": "^4.1.1",
|
||||||
"@codemirror/lang-html": "^6.4.9",
|
"@codemirror/lang-html": "^6.4.9",
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@codemirror/lang-liquid": "^6.2.1",
|
"@codemirror/lang-liquid": "^6.2.2",
|
||||||
"@hello-pangea/dnd": "^17.0.0",
|
"@hello-pangea/dnd": "^18.0.1",
|
||||||
"@libsql/client": "^0.14.0",
|
"@libsql/client": "^0.15.2",
|
||||||
"@mantine/core": "^7.13.4",
|
"@mantine/core": "^7.17.1",
|
||||||
"@sinclair/typebox": "^0.32.34",
|
"@mantine/hooks": "^7.17.1",
|
||||||
"@tanstack/react-form": "0.19.2",
|
"@sinclair/typebox": "^0.34.30",
|
||||||
"@uiw/react-codemirror": "^4.23.6",
|
"@tanstack/react-form": "^1.0.5",
|
||||||
"@xyflow/react": "^12.3.2",
|
"@uiw/react-codemirror": "^4.23.10",
|
||||||
"aws4fetch": "^1.0.18",
|
"@xyflow/react": "^12.4.4",
|
||||||
|
"aws4fetch": "^1.0.20",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"fast-xml-parser": "^4.4.0",
|
"fast-xml-parser": "^5.0.8",
|
||||||
"hono": "^4.6.12",
|
"hono": "^4.7.4",
|
||||||
"json-schema-form-react": "^0.0.2",
|
"json-schema-form-react": "^0.0.2",
|
||||||
"json-schema-library": "^10.0.0-rc7",
|
"json-schema-library": "^10.0.0-rc7",
|
||||||
"kysely": "^0.27.4",
|
"json-schema-to-ts": "^3.1.1",
|
||||||
"liquidjs": "^10.15.0",
|
"kysely": "^0.27.6",
|
||||||
|
"liquidjs": "^10.21.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"oauth4webapi": "^2.11.1",
|
"oauth4webapi": "^2.11.1",
|
||||||
"object-path-immutable": "^4.1.2",
|
"object-path-immutable": "^4.1.2",
|
||||||
"radix-ui": "^1.1.2",
|
"picocolors": "^1.1.1",
|
||||||
"json-schema-to-ts": "^3.1.1",
|
"radix-ui": "^1.1.3",
|
||||||
"swr": "^2.2.5"
|
"swr": "^2.3.3",
|
||||||
|
"wrangler": "^4.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.613.0",
|
"@aws-sdk/client-s3": "^3.758.0",
|
||||||
|
"@bluwy/giget-core": "^0.1.2",
|
||||||
"@dagrejs/dagre": "^1.1.4",
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
"@hono/typebox-validator": "^0.2.6",
|
"@hono/typebox-validator": "^0.3.2",
|
||||||
"@hono/vite-dev-server": "^0.17.0",
|
"@hono/vite-dev-server": "^0.19.0",
|
||||||
"@hono/zod-validator": "^0.4.1",
|
"@hookform/resolvers": "^4.1.3",
|
||||||
"@hookform/resolvers": "^3.9.1",
|
|
||||||
"@libsql/kysely-libsql": "^0.4.1",
|
"@libsql/kysely-libsql": "^0.4.1",
|
||||||
"@rjsf/core": "^5.22.2",
|
"@mantine/modals": "^7.17.1",
|
||||||
|
"@mantine/notifications": "^7.17.1",
|
||||||
|
"@playwright/test": "^1.51.1",
|
||||||
|
"@rjsf/core": "5.22.2",
|
||||||
"@tabler/icons-react": "3.18.0",
|
"@tabler/icons-react": "3.18.0",
|
||||||
"@types/node": "^22.10.0",
|
"@tailwindcss/postcss": "^4.0.12",
|
||||||
"@types/react": "^18.3.12",
|
"@tailwindcss/vite": "^4.0.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@vitejs/plugin-react": "^4.3.3",
|
"@testing-library/react": "^16.2.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"@types/node": "^22.13.10",
|
||||||
|
"@types/react": "^19.0.10",
|
||||||
|
"@types/react-dom": "^19.0.4",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"@vitest/coverage-v8": "^3.0.9",
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"esbuild-postcss": "^0.0.4",
|
"dotenv": "^16.4.7",
|
||||||
"jotai": "^2.10.1",
|
"jotai": "^2.12.2",
|
||||||
|
"jsdom": "^26.0.0",
|
||||||
|
"kysely-d1": "^0.3.0",
|
||||||
"open": "^10.1.0",
|
"open": "^10.1.0",
|
||||||
"openapi-types": "^12.1.3",
|
"openapi-types": "^12.1.3",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.5.3",
|
||||||
"postcss-preset-mantine": "^1.17.0",
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"react-hook-form": "^7.53.1",
|
"posthog-js-lite": "^3.4.2",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-hook-form": "^7.54.2",
|
||||||
"react-icons": "5.2.1",
|
"react-icons": "5.2.1",
|
||||||
"react-json-view-lite": "^2.0.1",
|
"react-json-view-lite": "^2.4.1",
|
||||||
"sql-formatter": "^15.4.9",
|
"sql-formatter": "^15.4.11",
|
||||||
"tailwind-merge": "^2.5.4",
|
"tailwind-merge": "^3.0.2",
|
||||||
"tailwindcss": "^3.4.14",
|
"tailwindcss": "^4.0.12",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tsc-alias": "^1.8.10",
|
"tsc-alias": "^1.8.11",
|
||||||
"tsup": "^8.3.5",
|
"tsup": "^8.4.0",
|
||||||
"vite": "^5.4.10",
|
"tsx": "^4.19.3",
|
||||||
"vite-plugin-static-copy": "^2.0.0",
|
"vite": "^6.2.1",
|
||||||
"vite-tsconfig-paths": "^5.0.1",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
"wouter": "^3.3.5"
|
"vitest": "^3.0.9",
|
||||||
|
"wouter": "^3.6.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@hono/node-server": "^1.13.7"
|
"@hono/node-server": "^1.13.8"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": ">=18",
|
"react": ">=19",
|
||||||
"react-dom": ">=18"
|
"react-dom": ">=19"
|
||||||
},
|
},
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"module": "./dist/index.js",
|
"module": "./dist/index.js",
|
||||||
@@ -109,47 +138,52 @@
|
|||||||
".": {
|
".": {
|
||||||
"types": "./dist/types/index.d.ts",
|
"types": "./dist/types/index.d.ts",
|
||||||
"import": "./dist/index.js",
|
"import": "./dist/index.js",
|
||||||
"require": "./dist/index.cjs"
|
"require": "./dist/index.js"
|
||||||
},
|
},
|
||||||
"./ui": {
|
"./ui": {
|
||||||
"types": "./dist/types/ui/index.d.ts",
|
"types": "./dist/types/ui/index.d.ts",
|
||||||
"import": "./dist/ui/index.js",
|
"import": "./dist/ui/index.js",
|
||||||
"require": "./dist/ui/index.cjs"
|
"require": "./dist/ui/index.js"
|
||||||
},
|
},
|
||||||
"./elements": {
|
"./elements": {
|
||||||
"types": "./dist/types/ui/elements/index.d.ts",
|
"types": "./dist/types/ui/elements/index.d.ts",
|
||||||
"import": "./dist/ui/elements/index.js",
|
"import": "./dist/ui/elements/index.js",
|
||||||
"require": "./dist/ui/elements/index.cjs"
|
"require": "./dist/ui/elements/index.js"
|
||||||
},
|
},
|
||||||
"./client": {
|
"./client": {
|
||||||
"types": "./dist/types/ui/client/index.d.ts",
|
"types": "./dist/types/ui/client/index.d.ts",
|
||||||
"import": "./dist/ui/client/index.js",
|
"import": "./dist/ui/client/index.js",
|
||||||
"require": "./dist/ui/client/index.cjs"
|
"require": "./dist/ui/client/index.js"
|
||||||
},
|
},
|
||||||
"./data": {
|
"./data": {
|
||||||
"types": "./dist/types/data/index.d.ts",
|
"types": "./dist/types/data/index.d.ts",
|
||||||
"import": "./dist/data/index.js",
|
"import": "./dist/data/index.js",
|
||||||
"require": "./dist/data/index.cjs"
|
"require": "./dist/data/index.js"
|
||||||
},
|
},
|
||||||
"./core": {
|
"./core": {
|
||||||
"types": "./dist/types/core/index.d.ts",
|
"types": "./dist/types/core/index.d.ts",
|
||||||
"import": "./dist/core/index.js",
|
"import": "./dist/core/index.js",
|
||||||
"require": "./dist/core/index.cjs"
|
"require": "./dist/core/index.js"
|
||||||
},
|
},
|
||||||
"./utils": {
|
"./utils": {
|
||||||
"types": "./dist/types/core/utils/index.d.ts",
|
"types": "./dist/types/core/utils/index.d.ts",
|
||||||
"import": "./dist/core/utils/index.js",
|
"import": "./dist/core/utils/index.js",
|
||||||
"require": "./dist/core/utils/index.cjs"
|
"require": "./dist/core/utils/index.js"
|
||||||
},
|
},
|
||||||
"./cli": {
|
"./cli": {
|
||||||
"types": "./dist/types/cli/index.d.ts",
|
"types": "./dist/types/cli/index.d.ts",
|
||||||
"import": "./dist/cli/index.js",
|
"import": "./dist/cli/index.js",
|
||||||
"require": "./dist/cli/index.cjs"
|
"require": "./dist/cli/index.js"
|
||||||
|
},
|
||||||
|
"./media": {
|
||||||
|
"types": "./dist/types/media/index.d.ts",
|
||||||
|
"import": "./dist/media/index.js",
|
||||||
|
"require": "./dist/media/index.js"
|
||||||
},
|
},
|
||||||
"./adapter/cloudflare": {
|
"./adapter/cloudflare": {
|
||||||
"types": "./dist/types/adapter/cloudflare/index.d.ts",
|
"types": "./dist/types/adapter/cloudflare/index.d.ts",
|
||||||
"import": "./dist/adapter/cloudflare/index.js",
|
"import": "./dist/adapter/cloudflare/index.js",
|
||||||
"require": "./dist/adapter/cloudflare/index.cjs"
|
"require": "./dist/adapter/cloudflare/index.js"
|
||||||
},
|
},
|
||||||
"./adapter": {
|
"./adapter": {
|
||||||
"types": "./dist/types/adapter/index.d.ts",
|
"types": "./dist/types/adapter/index.d.ts",
|
||||||
@@ -158,32 +192,37 @@
|
|||||||
"./adapter/vite": {
|
"./adapter/vite": {
|
||||||
"types": "./dist/types/adapter/vite/index.d.ts",
|
"types": "./dist/types/adapter/vite/index.d.ts",
|
||||||
"import": "./dist/adapter/vite/index.js",
|
"import": "./dist/adapter/vite/index.js",
|
||||||
"require": "./dist/adapter/vite/index.cjs"
|
"require": "./dist/adapter/vite/index.js"
|
||||||
},
|
},
|
||||||
"./adapter/nextjs": {
|
"./adapter/nextjs": {
|
||||||
"types": "./dist/types/adapter/nextjs/index.d.ts",
|
"types": "./dist/types/adapter/nextjs/index.d.ts",
|
||||||
"import": "./dist/adapter/nextjs/index.js",
|
"import": "./dist/adapter/nextjs/index.js",
|
||||||
"require": "./dist/adapter/nextjs/index.cjs"
|
"require": "./dist/adapter/nextjs/index.js"
|
||||||
},
|
},
|
||||||
"./adapter/remix": {
|
"./adapter/react-router": {
|
||||||
"types": "./dist/types/adapter/remix/index.d.ts",
|
"types": "./dist/types/adapter/react-router/index.d.ts",
|
||||||
"import": "./dist/adapter/remix/index.js",
|
"import": "./dist/adapter/react-router/index.js",
|
||||||
"require": "./dist/adapter/remix/index.cjs"
|
"require": "./dist/adapter/react-router/index.js"
|
||||||
},
|
},
|
||||||
"./adapter/bun": {
|
"./adapter/bun": {
|
||||||
"types": "./dist/types/adapter/bun/index.d.ts",
|
"types": "./dist/types/adapter/bun/index.d.ts",
|
||||||
"import": "./dist/adapter/bun/index.js",
|
"import": "./dist/adapter/bun/index.js",
|
||||||
"require": "./dist/adapter/bun/index.cjs"
|
"require": "./dist/adapter/bun/index.js"
|
||||||
},
|
},
|
||||||
"./adapter/node": {
|
"./adapter/node": {
|
||||||
"types": "./dist/types/adapter/node/index.d.ts",
|
"types": "./dist/types/adapter/node/index.d.ts",
|
||||||
"import": "./dist/adapter/node/index.js",
|
"import": "./dist/adapter/node/index.js",
|
||||||
"require": "./dist/adapter/node/index.cjs"
|
"require": "./dist/adapter/node/index.js"
|
||||||
},
|
},
|
||||||
"./adapter/astro": {
|
"./adapter/astro": {
|
||||||
"types": "./dist/types/adapter/astro/index.d.ts",
|
"types": "./dist/types/adapter/astro/index.d.ts",
|
||||||
"import": "./dist/adapter/astro/index.js",
|
"import": "./dist/adapter/astro/index.js",
|
||||||
"require": "./dist/adapter/astro/index.cjs"
|
"require": "./dist/adapter/astro/index.js"
|
||||||
|
},
|
||||||
|
"./adapter/aws": {
|
||||||
|
"types": "./dist/types/adapter/aws/index.d.ts",
|
||||||
|
"import": "./dist/adapter/aws/index.js",
|
||||||
|
"require": "./dist/adapter/aws/index.js"
|
||||||
},
|
},
|
||||||
"./dist/main.css": "./dist/ui/main.css",
|
"./dist/main.css": "./dist/ui/main.css",
|
||||||
"./dist/styles.css": "./dist/ui/styles.css",
|
"./dist/styles.css": "./dist/ui/styles.css",
|
||||||
@@ -213,6 +252,7 @@
|
|||||||
"cloudflare",
|
"cloudflare",
|
||||||
"nextjs",
|
"nextjs",
|
||||||
"remix",
|
"remix",
|
||||||
|
"react-router",
|
||||||
"astro",
|
"astro",
|
||||||
"bun",
|
"bun",
|
||||||
"node"
|
"node"
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
|
const baseUrl = process.env.TEST_URL || "http://localhost:28623";
|
||||||
|
const startCommand = process.env.TEST_START_COMMAND || "bun run dev";
|
||||||
|
const autoStart = ["1", "true", undefined].includes(process.env.TEST_AUTO_START);
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testMatch: "**/*.e2e-spec.ts",
|
||||||
|
testDir: "./e2e",
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
reporter: "html",
|
||||||
|
use: {
|
||||||
|
baseURL: baseUrl,
|
||||||
|
trace: "on-first-retry",
|
||||||
|
video: "on-first-retry",
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "chromium",
|
||||||
|
use: { ...devices["Desktop Chrome"] },
|
||||||
|
},
|
||||||
|
/* {
|
||||||
|
name: "firefox",
|
||||||
|
use: { ...devices["Desktop Firefox"] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "webkit",
|
||||||
|
use: { ...devices["Desktop Safari"] },
|
||||||
|
}, */
|
||||||
|
],
|
||||||
|
webServer: autoStart
|
||||||
|
? {
|
||||||
|
command: startCommand,
|
||||||
|
url: baseUrl,
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user