mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 08:06:00 +00:00
Compare commits
177 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b786fbcd13 | |||
| eabae5d3df | |||
| 8550aef606 | |||
| 01cb9d21b0 | |||
| c5ce79102e | |||
| 67ab3d2e38 | |||
| b1b88e37f1 | |||
| 7e61ff9995 | |||
| 58f9ea4cd7 | |||
| dde5ad365e | |||
| dda02807c1 | |||
| ef629321ab | |||
| 4f52537ea0 | |||
| 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 | |||
| 1e5c0dbc22 | |||
| d79da9d204 | |||
| 09d35d3cc0 | |||
| f7e6928dba | |||
| 3fa682bfe1 | |||
| 97b0ff24c8 | |||
| bb964dc835 | |||
| 5840ec6b89 | |||
| f29641c702 | |||
| 2e3ee65aa7 | |||
| 11a6f5c9b9 | |||
| 324d641410 | |||
| 02e7e1ca95 | |||
| 78d4f928ba | |||
| 0a5cc2ad98 | |||
| e14d528386 | |||
| 15003b004b | |||
| 9a4c2bd530 | |||
| ab61a1392c | |||
| 53d55448e6 | |||
| 46cf310ad6 | |||
| 8418231c43 | |||
| 4b3493a6f5 | |||
| f432473ed9 | |||
| 8f0cef1f37 | |||
| a8e78f984d | |||
| babeaae8d1 | |||
| 45d44f8177 | |||
| db476080ad | |||
| 158818520b | |||
| 4755288fed | |||
| 10db267164 | |||
| dfe7d55057 | |||
| c2b3316fcb | |||
| 86ba055f5e | |||
| 9d6ccda20d | |||
| 39c419c320 | |||
| 2d6d83ccb2 | |||
| ba1eb5fe08 | |||
| 123389786d | |||
| b65bb3b826 | |||
| 5b40d5eaf4 | |||
| 89b1fd3909 | |||
| aa7eac258b | |||
| 392a06c486 | |||
| 1beac5043b | |||
| 475488c910 | |||
| 9ddacb7ae5 | |||
| 3d1d52cbf8 | |||
| acc504cdc9 | |||
| d98d3351c0 | |||
| 3e606cb159 | |||
| cc76964d03 | |||
| b0c5f6307a | |||
| b3019d7333 | |||
| 2265451d4b | |||
| ba936604a3 | |||
| 7dd42fbff3 | |||
| f64e5dac03 |
@@ -0,0 +1,26 @@
|
|||||||
|
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: latest
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: ./app
|
||||||
|
run: bun install
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
working-directory: ./app
|
||||||
|
run: bun run test
|
||||||
@@ -19,6 +19,7 @@ packages/media/.env
|
|||||||
.history
|
.history
|
||||||
**/*/.db/*
|
**/*/.db/*
|
||||||
**/*/.configs/*
|
**/*/.configs/*
|
||||||
|
**/*/.template/*
|
||||||
**/*/*.db
|
**/*/*.db
|
||||||
**/*/*.db-shm
|
**/*/*.db-shm
|
||||||
**/*/*.db-wal
|
**/*/*.db-wal
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
[](https://npmjs.org/package/bknd
|
[](https://npmjs.org/package/bknd)
|
||||||
"View this project on NPM")
|
|
||||||
[](https://www.npmjs.com/package/bknd)
|
[](https://www.npmjs.com/package/bknd)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
bknd simplifies app development by providing fully functional backend for data management,
|
<p align="center" width="100%">
|
||||||
authentication, workflows and media. Since it's lightweight and built on Web Standards, it can
|
<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">
|
||||||
be deployed nearly anywhere, including running inside your framework of choice. No more
|
<strong>⭐ Live Demo</strong>
|
||||||
deploying multiple separate services!
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
bknd simplifies app development by providing a fully functional backend for database management, authentication, media and workflows. Being lightweight and built on Web Standards, it can be deployed nearly anywhere, including running inside your framework of choice. No more deploying multiple separate services!
|
||||||
|
|
||||||
**For documentation and examples, please visit https://docs.bknd.io.**
|
**For documentation and examples, please visit https://docs.bknd.io.**
|
||||||
|
|
||||||
@@ -15,33 +17,118 @@ deploying multiple separate services!
|
|||||||
> Please keep in mind that **bknd** is still under active development
|
> Please keep in mind that **bknd** is still under active development
|
||||||
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
|
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
|
||||||
|
|
||||||
## ✨ Features
|
## Size
|
||||||
**📊 Data**: Define, query, and control your data with ease.
|

|
||||||
- Define entities with fields and relationships, synced directly to your database.
|

|
||||||
- Supported field types: `primary`, `text`, `number`, `date`, `boolean`, `enum`, `json`, `jsonschema`.
|

|
||||||
- Relationship types: `one-to-one`, `many-to-one`, `many-to-many`, and `polymorphic`.
|

|
||||||
- Advanced querying with the **Repository**: filtering, sorting, pagination, and relational data handling.
|
|
||||||
- Seamlessly manage data with mutators and a robust event system.
|
|
||||||
- Extend database functionality with batching, introspection, and support for multiple SQL dialects.
|
|
||||||
|
|
||||||
**🔐 Auth**: Easily implement reliable authentication strategies.
|
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.
|
||||||
- Built-in `user` entity with customizable fields.
|
|
||||||
- Supports multiple authentication strategies:
|
|
||||||
- Email/password (with hashed storage).
|
|
||||||
- OAuth/OIDC (Google, GitHub, and more).
|
|
||||||
- Secure JWT generation and session management.
|
|
||||||
|
|
||||||
**🖼️ Media**: Effortlessly manage and serve all your media files.
|
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).
|
||||||
- Upload files with ease.
|
|
||||||
- Adapter-based support for S3, S3-compatible storage (e.g., R2, Tigris), and Cloudinary.
|
|
||||||
|
|
||||||
**🔄 Flows** (UI integration coming soon!): Design and run workflows with seamless automation.
|
## Motivation
|
||||||
- Create and run workflows with trigger-based automation:
|
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.
|
||||||
- Manual triggers or events from data, auth, media, or server actions.
|
|
||||||
- HTTP triggers for external integrations.
|
**The solution:** A backend system that only assumes and implements primitive details, integrates into multiple environments, and adheres to industry standards.
|
||||||
- Define tasks in sequence, parallel, or loops, with conditional execution.
|
|
||||||
- Use reusable sub-workflows to organize complex processes.
|
## Features
|
||||||
- Leverage OpenAPI specifications for API-based tasks.
|
* ⚡ Instant backend with full REST API:
|
||||||
|
* **Data**: Define, query, and control your data with ease.
|
||||||
|
* **Auth**: Easily implement reliable authentication strategies.
|
||||||
|
* **Media**: Effortlessly manage and serve all your media files.
|
||||||
|
* **Flows**: Design and run workflows with seamless automation. (UI integration coming soon!)
|
||||||
|
* 🌐 Built on Web Standards for maximum compatibility
|
||||||
|
* 🏃♂️ Multiple run modes
|
||||||
|
* standalone using the CLI
|
||||||
|
* using a JavaScript runtime (Node, Bun, workerd)
|
||||||
|
* using a React framework (Astro, Remix, Next.js)
|
||||||
|
* 📦 Official API and React SDK with type-safety
|
||||||
|
* ⚛️ React elements for auto-configured authentication and media components
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
The package is mainly split into 4 parts, each serving a specific purpose:
|
||||||
|
|
||||||
|
| Import | Purpose |
|
||||||
|
|-----------------------------|------------------------------------------------------|
|
||||||
|
| `bknd`<br/>`bknd/adapter/*` | Backend including APIs and adapters |
|
||||||
|
| `bknd/ui` | Admin UI components for react frameworks |
|
||||||
|
| `bknd/client` | TypeScript SDK and React hooks for the API endpoints |
|
||||||
|
| `bknd/elements` | React components for authentication and media |
|
||||||
|
|
||||||
|
|
||||||
|
### The backend (`bknd`)
|
||||||
|
Serve the backend as an API for any JS runtime or framework. The latter is especially handy, as it allows you to deploy your frontend and backend bundled together. Furthermore it allows adding additional logic in a way you're already familar with. Just add another route and you're good to go.
|
||||||
|
|
||||||
|
Here is an example of serving the API using node:
|
||||||
|
```js index.js
|
||||||
|
import { serve } from "bknd/adapter/node"
|
||||||
|
serve();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integrated admin UI (`bknd/ui`)
|
||||||
|
The admin UI allows to manage your data including full configuration of your backend using a graphical user interface. Using `vite`, your admin route looks like this:
|
||||||
|
```tsx
|
||||||
|
import { Admin } from "bknd/ui"
|
||||||
|
import "bknd/dist/styles.css";
|
||||||
|
|
||||||
|
export default function AdminPage() {
|
||||||
|
return <Admin />
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using the REST API or TypeScript SDK (`bknd/client`)
|
||||||
|
If you're not using a JavaScript environment, you can still access any endpoint using the REST API:
|
||||||
|
```bash
|
||||||
|
curl -XGET <your-endpoint>/api/data/entity/<entity>
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{ "id": 1, ... },
|
||||||
|
{ "id": 2, ... }
|
||||||
|
],
|
||||||
|
"meta": { /* ... */ }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In a JavaScript environment, you can use the TypeScript SDK with type-safety. The above example would look like this:
|
||||||
|
```ts
|
||||||
|
import { Api } from "bknd/client";
|
||||||
|
|
||||||
|
const api = new Api({ host: "<endpoint>" });
|
||||||
|
const { data } = await api.data.readMany("<entity>");
|
||||||
|
```
|
||||||
|
|
||||||
|
If you're using React, there are 2 hooks exposed (`useApi`, `useEntity`), as well as an `swr` wrapper around each (`useApiQuery`, `useEntityQuery`). The `swr` wrapped hooks automatically handled query invalidation:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useEntityQuery } from "bknd/client";
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const { data } = useEntityQuery("todos");
|
||||||
|
return <ul>
|
||||||
|
{data?.map(todo => (
|
||||||
|
<li key={todo.id}>{todo.name}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### React elements (`bknd/elements`)
|
||||||
|
You don't have to figure out API details to include media uploads to your app. For an user avatar upload, this is all you need:
|
||||||
|
```tsx
|
||||||
|
import { Media } from "bknd/elements"
|
||||||
|
import "bknd/dist/main.css"
|
||||||
|
|
||||||
|
export function UserAvatar() {
|
||||||
|
return <Media.Dropzone
|
||||||
|
entity={{ name: "users", id: 1, field: "avatar" }}
|
||||||
|
maxItems={1}
|
||||||
|
overwrite
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
The import path also exports components for login and registration forms which are automatically pointed to the `bknd` defaults.
|
||||||
|
|
||||||
|
|
||||||
## 🚀 Quick start
|
## 🚀 Quick start
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
tmp/*
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
@@ -0,0 +1,71 @@
|
|||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
import { sign } from "hono/jwt";
|
||||||
|
import { Api } from "../../src/Api";
|
||||||
|
|
||||||
|
describe("Api", async () => {
|
||||||
|
it("should construct without options", () => {
|
||||||
|
const api = new Api();
|
||||||
|
expect(api.baseUrl).toBe("http://localhost");
|
||||||
|
expect(api.isAuthVerified()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ignore force verify if no claims given", () => {
|
||||||
|
const api = new Api({ verified: true });
|
||||||
|
expect(api.baseUrl).toBe("http://localhost");
|
||||||
|
expect(api.isAuthVerified()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should construct from request (token)", async () => {
|
||||||
|
const token = await sign({ foo: "bar" }, "1234");
|
||||||
|
const request = new Request("http://example.com/test", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const api = new Api({ request });
|
||||||
|
expect(api.isAuthVerified()).toBe(false);
|
||||||
|
|
||||||
|
const params = api.getParams();
|
||||||
|
expect(params.token).toBe(token);
|
||||||
|
expect(params.token_transport).toBe("header");
|
||||||
|
expect(params.host).toBe("http://example.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should construct from request (cookie)", async () => {
|
||||||
|
const token = await sign({ foo: "bar" }, "1234");
|
||||||
|
const request = new Request("http://example.com/test", {
|
||||||
|
headers: {
|
||||||
|
Cookie: `auth=${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const api = new Api({ request });
|
||||||
|
expect(api.isAuthVerified()).toBe(false);
|
||||||
|
|
||||||
|
const params = api.getParams();
|
||||||
|
console.log(params);
|
||||||
|
expect(params.token).toBe(token);
|
||||||
|
expect(params.token_transport).toBe("cookie");
|
||||||
|
expect(params.host).toBe("http://example.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should construct from token", async () => {
|
||||||
|
const token = await sign({ foo: "bar" }, "1234");
|
||||||
|
const api = new Api({ token });
|
||||||
|
expect(api.isAuthVerified()).toBe(false);
|
||||||
|
|
||||||
|
const params = api.getParams();
|
||||||
|
expect(params.token).toBe(token);
|
||||||
|
expect(params.token_transport).toBe("header");
|
||||||
|
expect(params.host).toBe("http://localhost");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prefer host when request is given", async () => {
|
||||||
|
const request = new Request("http://example.com/test");
|
||||||
|
const api = new Api({ request, host: "http://another.com" });
|
||||||
|
|
||||||
|
const params = api.getParams();
|
||||||
|
expect(params.token).toBeUndefined();
|
||||||
|
expect(params.token_transport).toBe("header");
|
||||||
|
expect(params.host).toBe("http://another.com");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -17,16 +17,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 +39,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 +58,155 @@ 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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" })).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",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -27,10 +27,7 @@ describe("ModuleApi", () => {
|
|||||||
|
|
||||||
it("fetches endpoint", async () => {
|
it("fetches endpoint", async () => {
|
||||||
const app = new Hono().get("/endpoint", (c) => c.json({ foo: "bar" }));
|
const app = new Hono().get("/endpoint", (c) => c.json({ foo: "bar" }));
|
||||||
const api = new Api({ host });
|
const api = new Api({ host }, app.request as typeof fetch);
|
||||||
|
|
||||||
// @ts-expect-error it's protected
|
|
||||||
api.fetcher = app.request as typeof fetch;
|
|
||||||
|
|
||||||
const res = await api.get("/endpoint");
|
const res = await api.get("/endpoint");
|
||||||
expect(res.res.ok).toEqual(true);
|
expect(res.res.ok).toEqual(true);
|
||||||
@@ -41,10 +38,7 @@ describe("ModuleApi", () => {
|
|||||||
|
|
||||||
it("has accessible request", async () => {
|
it("has accessible request", async () => {
|
||||||
const app = new Hono().get("/endpoint", (c) => c.json({ foo: "bar" }));
|
const app = new Hono().get("/endpoint", (c) => c.json({ foo: "bar" }));
|
||||||
const api = new Api({ host });
|
const api = new Api({ host }, app.request as typeof fetch);
|
||||||
|
|
||||||
// @ts-expect-error it's protected
|
|
||||||
api.fetcher = app.request as typeof fetch;
|
|
||||||
|
|
||||||
const promise = api.get("/endpoint");
|
const promise = api.get("/endpoint");
|
||||||
expect(promise.request).toBeDefined();
|
expect(promise.request).toBeDefined();
|
||||||
@@ -67,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");
|
||||||
@@ -81,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());
|
||||||
});
|
});
|
||||||
@@ -95,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,54 @@
|
|||||||
|
import { describe, expect, mock, test } from "bun:test";
|
||||||
|
import type { ModuleBuildContext } from "../../src";
|
||||||
|
import { type 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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");
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ describe("OAuthStrategy", async () => {
|
|||||||
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";
|
||||||
@@ -21,7 +21,7 @@ describe("OAuthStrategy", async () => {
|
|||||||
|
|
||||||
const request = await strategy.request({
|
const request = await strategy.request({
|
||||||
redirect_uri,
|
redirect_uri,
|
||||||
state
|
state,
|
||||||
});
|
});
|
||||||
|
|
||||||
const server = Bun.serve({
|
const server = Bun.serve({
|
||||||
@@ -31,13 +31,13 @@ describe("OAuthStrategy", async () => {
|
|||||||
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!");
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
console.log("request", request);
|
console.log("request", request);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -83,8 +83,8 @@ describe("EventManager", async () => {
|
|||||||
const emgr = new EventManager(
|
const emgr = new EventManager(
|
||||||
{ InformationalEvent },
|
{ InformationalEvent },
|
||||||
{
|
{
|
||||||
asyncExecutor
|
asyncExecutor,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
emgr.onEvent(InformationalEvent, async () => {});
|
emgr.onEvent(InformationalEvent, async () => {});
|
||||||
@@ -98,8 +98,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 +111,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");
|
||||||
|
|
||||||
@@ -139,7 +140,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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { after, beforeEach, describe, test } from "node:test";
|
|||||||
import { Miniflare } from "miniflare";
|
import { Miniflare } from "miniflare";
|
||||||
import {
|
import {
|
||||||
CloudflareKVCacheItem,
|
CloudflareKVCacheItem,
|
||||||
CloudflareKVCachePool
|
CloudflareKVCachePool,
|
||||||
} from "../../../src/core/cache/adapters/CloudflareKvCache";
|
} from "../../../src/core/cache/adapters/CloudflareKvCache";
|
||||||
import { runTests } from "./cache-test-suite";
|
import { runTests } from "./cache-test-suite";
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ describe("CloudflareKv", async () => {
|
|||||||
mf = new Miniflare({
|
mf = new Miniflare({
|
||||||
modules: true,
|
modules: true,
|
||||||
script: "export default { async fetch() { return new Response(null); } }",
|
script: "export default { async fetch() { return new Response(null); } }",
|
||||||
kvNamespaces: ["TEST"]
|
kvNamespaces: ["TEST"],
|
||||||
});
|
});
|
||||||
const kv = await mf.getKVNamespace("TEST");
|
const kv = await mf.getKVNamespace("TEST");
|
||||||
return new CloudflareKVCachePool(kv as any);
|
return new CloudflareKVCachePool(kv as any);
|
||||||
@@ -45,10 +45,10 @@ describe("CloudflareKv", async () => {
|
|||||||
},
|
},
|
||||||
toBeUndefined() {
|
toBeUndefined() {
|
||||||
assert.equal(actual, undefined);
|
assert.equal(actual, undefined);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
after(async () => {
|
after(async () => {
|
||||||
|
|||||||
+2
-2
@@ -9,7 +9,7 @@ describe("MemoryCache", () => {
|
|||||||
tester: {
|
tester: {
|
||||||
test,
|
test,
|
||||||
beforeEach,
|
beforeEach,
|
||||||
expect
|
expect,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,35 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
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) {
|
||||||
|
|||||||
+162
-15
@@ -1,7 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Perf } from "../../src/core/utils";
|
import { Perf, datetimeStringUTC, isBlob, ucFirst } from "../../src/core/utils";
|
||||||
import * as reqres from "../../src/core/utils/reqres";
|
import * as utils from "../../src/core/utils";
|
||||||
import * as strings from "../../src/core/utils/strings";
|
|
||||||
|
|
||||||
async function wait(ms: number) {
|
async function wait(ms: number) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@@ -13,33 +12,33 @@ describe("Core Utils", async () => {
|
|||||||
describe("[core] strings", async () => {
|
describe("[core] strings", async () => {
|
||||||
test("objectToKeyValueArray", async () => {
|
test("objectToKeyValueArray", async () => {
|
||||||
const obj = { a: 1, b: 2, c: 3 };
|
const obj = { a: 1, b: 2, c: 3 };
|
||||||
const result = strings.objectToKeyValueArray(obj);
|
const result = utils.objectToKeyValueArray(obj);
|
||||||
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 },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("snakeToPascalWithSpaces", async () => {
|
test("snakeToPascalWithSpaces", async () => {
|
||||||
const result = strings.snakeToPascalWithSpaces("snake_to_pascal");
|
const result = utils.snakeToPascalWithSpaces("snake_to_pascal");
|
||||||
expect(result).toBe("Snake To Pascal");
|
expect(result).toBe("Snake To Pascal");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("randomString", async () => {
|
test("randomString", async () => {
|
||||||
const result = strings.randomString(10);
|
const result = utils.randomString(10);
|
||||||
expect(result).toHaveLength(10);
|
expect(result).toHaveLength(10);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("pascalToKebab", async () => {
|
test("pascalToKebab", async () => {
|
||||||
const result = strings.pascalToKebab("PascalCase");
|
const result = utils.pascalToKebab("PascalCase");
|
||||||
expect(result).toBe("pascal-case");
|
expect(result).toBe("pascal-case");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("replaceSimplePlaceholders", async () => {
|
test("replaceSimplePlaceholders", async () => {
|
||||||
const str = "Hello, {$name}!";
|
const str = "Hello, {$name}!";
|
||||||
const vars = { name: "John" };
|
const vars = { name: "John" };
|
||||||
const result = strings.replaceSimplePlaceholders(str, vars);
|
const result = utils.replaceSimplePlaceholders(str, vars);
|
||||||
expect(result).toBe("Hello, John!");
|
expect(result).toBe("Hello, John!");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -49,33 +48,84 @@ describe("Core Utils", async () => {
|
|||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
headers.append("Content-Type", "application/json");
|
headers.append("Content-Type", "application/json");
|
||||||
headers.append("Authorization", "Bearer 123");
|
headers.append("Authorization", "Bearer 123");
|
||||||
const obj = reqres.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",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("replaceUrlParam", () => {
|
test("replaceUrlParam", () => {
|
||||||
const url = "/api/:id/:name";
|
const url = "/api/:id/:name";
|
||||||
const params = { id: "123", name: "test" };
|
const params = { id: "123", name: "test" };
|
||||||
const result = reqres.replaceUrlParam(url, params);
|
const result = utils.replaceUrlParam(url, params);
|
||||||
expect(result).toBe("/api/123/test");
|
expect(result).toBe("/api/123/test");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("encode", () => {
|
test("encode", () => {
|
||||||
const obj = { id: "123", name: "test" };
|
const obj = { id: "123", name: "test" };
|
||||||
const result = reqres.encodeSearch(obj);
|
const result = utils.encodeSearch(obj);
|
||||||
expect(result).toBe("id=123&name=test");
|
expect(result).toBe("id=123&name=test");
|
||||||
|
|
||||||
const obj2 = { id: "123", name: ["test1", "test2"] };
|
const obj2 = { id: "123", name: ["test1", "test2"] };
|
||||||
const result2 = reqres.encodeSearch(obj2);
|
const result2 = utils.encodeSearch(obj2);
|
||||||
expect(result2).toBe("id=123&name=test1&name=test2");
|
expect(result2).toBe("id=123&name=test1&name=test2");
|
||||||
|
|
||||||
const obj3 = { id: "123", name: { test: "test" } };
|
const obj3 = { id: "123", name: { test: "test" } };
|
||||||
const result3 = reqres.encodeSearch(obj3, { encode: true });
|
const result3 = utils.encodeSearch(obj3, { encode: true });
|
||||||
expect(result3).toBe("id=123&name=%7B%22test%22%3A%22test%22%7D");
|
expect(result3).toBe("id=123&name=%7B%22test%22%3A%22test%22%7D");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("guards", () => {
|
||||||
|
const types = {
|
||||||
|
blob: new Blob(),
|
||||||
|
file: new File([""], "file.txt"),
|
||||||
|
stream: new ReadableStream(),
|
||||||
|
arrayBuffer: new ArrayBuffer(10),
|
||||||
|
arrayBufferView: new Uint8Array(new ArrayBuffer(10)),
|
||||||
|
};
|
||||||
|
|
||||||
|
const fns = [
|
||||||
|
[utils.isReadableStream, "stream"],
|
||||||
|
[utils.isBlob, "blob", ["stream", "arrayBuffer", "arrayBufferView"]],
|
||||||
|
[utils.isFile, "file", ["stream", "arrayBuffer", "arrayBufferView"]],
|
||||||
|
[utils.isArrayBuffer, "arrayBuffer"],
|
||||||
|
[utils.isArrayBufferView, "arrayBufferView"],
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const additional = [0, 0.0, "", null, undefined, {}, []];
|
||||||
|
|
||||||
|
for (const [fn, type, _to_test] of fns) {
|
||||||
|
test(`is${ucFirst(type)}`, () => {
|
||||||
|
const to_test = _to_test ?? (Object.keys(types) as string[]);
|
||||||
|
for (const key of to_test) {
|
||||||
|
const value = types[key as keyof typeof types];
|
||||||
|
const result = fn(value);
|
||||||
|
expect(result).toBe(key === type);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const value of additional) {
|
||||||
|
const result = fn(value);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getContentName", () => {
|
||||||
|
const name = "test.json";
|
||||||
|
const text = "attachment; filename=" + name;
|
||||||
|
const headers = new Headers({
|
||||||
|
"Content-Disposition": text,
|
||||||
|
});
|
||||||
|
const request = new Request("http://example.com", {
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(utils.getContentName(text)).toBe(name);
|
||||||
|
expect(utils.getContentName(headers)).toBe(name);
|
||||||
|
expect(utils.getContentName(request)).toBe(name);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("perf", async () => {
|
describe("perf", async () => {
|
||||||
@@ -108,4 +158,101 @@ describe("Core Utils", async () => {
|
|||||||
expect(count).toBe(2);
|
expect(count).toBe(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("objects", () => {
|
||||||
|
test("omitKeys", () => {
|
||||||
|
const objects = [
|
||||||
|
[{ a: 1, b: 2, c: 3 }, ["a"], { b: 2, 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 }, ["a", "b"], { c: 3 }],
|
||||||
|
[{ a: 1, b: 2, c: 3 }, ["a", "b", "c"], {}],
|
||||||
|
] as [object, string[], object][];
|
||||||
|
|
||||||
|
for (const [obj, keys, expected] of objects) {
|
||||||
|
const result = utils.omitKeys(obj, keys as any);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("isEqual", () => {
|
||||||
|
const objects = [
|
||||||
|
[1, 1, true],
|
||||||
|
[1, "1", false],
|
||||||
|
[1, 2, false],
|
||||||
|
["1", "1", true],
|
||||||
|
["1", "2", false],
|
||||||
|
[true, true, true],
|
||||||
|
[true, false, false],
|
||||||
|
[false, false, true],
|
||||||
|
[1, Number.NaN, false],
|
||||||
|
[Number.NaN, Number.NaN, true],
|
||||||
|
[null, null, true],
|
||||||
|
[null, undefined, false],
|
||||||
|
[undefined, undefined, true],
|
||||||
|
[new Map([["a", 1]]), new Map([["a", 1]]), true],
|
||||||
|
[new Map([["a", 1]]), new Map([["a", 2]]), false],
|
||||||
|
[new Map([["a", 1]]), new Map([["b", 1]]), false],
|
||||||
|
[
|
||||||
|
new Map([["a", 1]]),
|
||||||
|
new Map([
|
||||||
|
["a", 1],
|
||||||
|
["b", 2],
|
||||||
|
]),
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
[{ a: 1 }, { a: 1 }, true],
|
||||||
|
[{ a: 1 }, { a: 2 }, false],
|
||||||
|
[{ a: 1 }, { b: 1 }, false],
|
||||||
|
[{ a: "1" }, { a: "1" }, true],
|
||||||
|
[{ a: "1" }, { a: "2" }, false],
|
||||||
|
[{ a: "1" }, { b: "1" }, false],
|
||||||
|
[{ a: 1 }, { a: 1, b: 2 }, false],
|
||||||
|
[{ a: [1, 2, 3] }, { a: [1, 2, 3] }, true],
|
||||||
|
[{ a: [1, 2, 3] }, { a: [1, 2, 4] }, false],
|
||||||
|
[{ a: [1, 2, 3] }, { a: [1, 2, 3, 4] }, false],
|
||||||
|
[{ a: { b: 1 } }, { a: { b: 1 } }, true],
|
||||||
|
[{ a: { b: 1 } }, { a: { b: 2 } }, false],
|
||||||
|
[{ a: { b: 1 } }, { a: { c: 1 } }, false],
|
||||||
|
[{ a: { b: 1 } }, { a: { b: 1, c: 2 } }, false],
|
||||||
|
[[1, 2, 3], [1, 2, 3], true],
|
||||||
|
[[1, 2, 3], [1, 2, 4], false],
|
||||||
|
[[1, 2, 3], [1, 2, 3, 4], false],
|
||||||
|
[[{ a: 1 }], [{ a: 1 }], true],
|
||||||
|
[[{ a: 1 }], [{ a: 2 }], false],
|
||||||
|
[[{ a: 1 }], [{ b: 1 }], false],
|
||||||
|
] as [any, any, boolean][];
|
||||||
|
|
||||||
|
for (const [a, b, expected] of objects) {
|
||||||
|
const result = utils.isEqual(a, b);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getPath", () => {
|
||||||
|
const tests = [
|
||||||
|
[{ a: 1, b: 2, c: 3 }, "a", 1],
|
||||||
|
[{ a: 1, b: 2, c: 3 }, "b", 2],
|
||||||
|
[{ a: { b: 1 } }, "a.b", 1],
|
||||||
|
[{ a: { b: 1 } }, "a.b.c", null, null],
|
||||||
|
[{ a: { b: 1 } }, "a.b.c", 1, 1],
|
||||||
|
[[[1]], "0.0", 1],
|
||||||
|
] as [object, string, any, any][];
|
||||||
|
|
||||||
|
for (const [obj, path, expected, defaultValue] of tests) {
|
||||||
|
const result = utils.getPath(obj, path, defaultValue);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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,24 +105,41 @@ 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
|
||||||
|
{
|
||||||
|
const output = { with: { images: {} } };
|
||||||
|
decode({ with: "images" }, output);
|
||||||
|
decode({ with: '["images"]' }, output);
|
||||||
|
decode({ with: ["images"] }, output);
|
||||||
|
decode({ with: { images: {} } }, output);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const output = { with: { images: {}, comments: {} } };
|
||||||
|
decode({ with: "images,comments" }, output);
|
||||||
|
decode({ with: ["images", "comments"] }, output);
|
||||||
|
decode({ with: '["images", "comments"]' }, output);
|
||||||
|
decode({ with: { images: {}, comments: {} } }, output);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,7 +22,7 @@ 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
|
||||||
@@ -31,14 +31,14 @@ describe("Relations", async () => {
|
|||||||
.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
|
||||||
@@ -47,7 +47,7 @@ describe("Relations", async () => {
|
|||||||
.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();
|
||||||
});
|
});
|
||||||
@@ -303,7 +303,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 +318,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 +329,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 +351,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 +363,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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
LibsqlConnection,
|
LibsqlConnection,
|
||||||
ManyToOneRelation,
|
ManyToOneRelation,
|
||||||
RepositoryEvents,
|
RepositoryEvents,
|
||||||
TextField
|
TextField,
|
||||||
} from "../../../src/data";
|
} from "../../../src/data";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
|
|
||||||
@@ -70,13 +70,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 +93,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 +102,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;
|
||||||
@@ -183,7 +183,7 @@ 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>();
|
||||||
|
|||||||
@@ -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,8 +101,8 @@ 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
|
||||||
@@ -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,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ describe("[data] EnumField", async () => {
|
|||||||
runBaseFieldTests(
|
runBaseFieldTests(
|
||||||
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" })
|
() => new EnumField("test", { options: options(["a", "b"]), default_value: "c" }),
|
||||||
).toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ describe("[data] EnumField", async () => {
|
|||||||
const field = new EnumField("test", {
|
const field = new EnumField("test", {
|
||||||
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");
|
||||||
|
|||||||
@@ -24,20 +24,20 @@ describe("[data] Field", 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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
EntityIndex,
|
EntityIndex,
|
||||||
type EntityManager,
|
type EntityManager,
|
||||||
Field,
|
Field,
|
||||||
type SchemaResponse
|
type SchemaResponse,
|
||||||
} from "../../../../src/data";
|
} from "../../../../src/data";
|
||||||
|
|
||||||
class TestField extends Field {
|
class TestField extends Field {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ describe("[data] JsonField", async () => {
|
|||||||
runBaseFieldTests(JsonField, {
|
runBaseFieldTests(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 () => {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function transformPersist(field: Field, value: any, context?: TActionCont
|
|||||||
export function runBaseFieldTests(
|
export function runBaseFieldTests(
|
||||||
fieldClass: ConstructableField,
|
fieldClass: ConstructableField,
|
||||||
config: FieldTestConfig,
|
config: FieldTestConfig,
|
||||||
_requiredConfig: any = {}
|
_requiredConfig: any = {},
|
||||||
) {
|
) {
|
||||||
const noConfigField = new fieldClass("no_config", _requiredConfig);
|
const noConfigField = new fieldClass("no_config", _requiredConfig);
|
||||||
const fillable = new fieldClass("fillable", { ..._requiredConfig, fillable: true });
|
const fillable = new fieldClass("fillable", { ..._requiredConfig, fillable: true });
|
||||||
@@ -29,7 +29,7 @@ export function runBaseFieldTests(
|
|||||||
..._requiredConfig,
|
..._requiredConfig,
|
||||||
fillable: true,
|
fillable: true,
|
||||||
required: true,
|
required: true,
|
||||||
default_value: config.defaultValue
|
default_value: config.defaultValue,
|
||||||
});
|
});
|
||||||
|
|
||||||
test("schema", () => {
|
test("schema", () => {
|
||||||
@@ -37,7 +37,7 @@ export function runBaseFieldTests(
|
|||||||
expect(noConfigField.schema(null as any)).toEqual([
|
expect(noConfigField.schema(null as any)).toEqual([
|
||||||
"no_config",
|
"no_config",
|
||||||
config.schemaType,
|
config.schemaType,
|
||||||
expect.any(Function)
|
expect.any(Function),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ export function runBaseFieldTests(
|
|||||||
//order: 1,
|
//order: 1,
|
||||||
fillable: true,
|
fillable: true,
|
||||||
required: false,
|
required: false,
|
||||||
hidden: false
|
hidden: false,
|
||||||
//virtual: false,
|
//virtual: false,
|
||||||
//default_value: undefined
|
//default_value: undefined
|
||||||
};
|
};
|
||||||
@@ -105,20 +105,20 @@ export function runBaseFieldTests(
|
|||||||
const json = field.toJSON();
|
const json = field.toJSON();
|
||||||
return {
|
return {
|
||||||
...json,
|
...json,
|
||||||
config: omit(json.config, ["html"])
|
config: omit(json.config, ["html"]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(fieldJson(noConfigField)).toEqual({
|
expect(fieldJson(noConfigField)).toEqual({
|
||||||
//name: "no_config",
|
//name: "no_config",
|
||||||
type: noConfigField.type,
|
type: noConfigField.type,
|
||||||
config: _config
|
config: _config,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(fieldJson(fillable)).toEqual({
|
expect(fieldJson(fillable)).toEqual({
|
||||||
//name: "fillable",
|
//name: "fillable",
|
||||||
type: noConfigField.type,
|
type: noConfigField.type,
|
||||||
config: _config
|
config: _config,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(fieldJson(required)).toEqual({
|
expect(fieldJson(required)).toEqual({
|
||||||
@@ -126,8 +126,8 @@ export function runBaseFieldTests(
|
|||||||
type: required.type,
|
type: required.type,
|
||||||
config: {
|
config: {
|
||||||
..._config,
|
..._config,
|
||||||
required: true
|
required: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(fieldJson(hidden)).toEqual({
|
expect(fieldJson(hidden)).toEqual({
|
||||||
@@ -135,8 +135,8 @@ export function runBaseFieldTests(
|
|||||||
type: required.type,
|
type: required.type,
|
||||||
config: {
|
config: {
|
||||||
..._config,
|
..._config,
|
||||||
hidden: true
|
hidden: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(fieldJson(dflt)).toEqual({
|
expect(fieldJson(dflt)).toEqual({
|
||||||
@@ -144,8 +144,8 @@ export function runBaseFieldTests(
|
|||||||
type: dflt.type,
|
type: dflt.type,
|
||||||
config: {
|
config: {
|
||||||
..._config,
|
..._config,
|
||||||
default_value: config.defaultValue
|
default_value: config.defaultValue,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(fieldJson(requiredAndDefault)).toEqual({
|
expect(fieldJson(requiredAndDefault)).toEqual({
|
||||||
@@ -155,8 +155,8 @@ export function runBaseFieldTests(
|
|||||||
..._config,
|
..._config,
|
||||||
fillable: true,
|
fillable: true,
|
||||||
required: true,
|
required: true,
|
||||||
default_value: config.defaultValue
|
default_value: config.defaultValue,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -17,7 +17,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 +27,7 @@ export function getDummyConnection(memory: boolean = true) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
dummyConnection,
|
dummyConnection,
|
||||||
afterAllCleanup
|
afterAllCleanup,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +39,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 +68,6 @@ 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`;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
import { afterAll, 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 } from "../helper";
|
||||||
|
|
||||||
@@ -15,25 +16,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 +42,31 @@ 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({
|
||||||
initialConfig: {
|
initialConfig: {
|
||||||
auth: configs.auth
|
auth: configs.auth,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
app.emgr.onEvent(
|
app.emgr.onEvent(
|
||||||
@@ -74,7 +75,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 +94,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 +118,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 +133,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 +211,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 "../../src/media/storage/adapters/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
|
|
||||||
{
|
|
||||||
enabled: true,
|
|
||||||
adapter: {
|
|
||||||
type: "s3",
|
|
||||||
config: {
|
|
||||||
access_key: process.env.R2_ACCESS_KEY as string,
|
|
||||||
secret_access_key: process.env.R2_SECRET_ACCESS_KEY as string,
|
|
||||||
url: process.env.R2_URL as string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ctx
|
|
||||||
);
|
|
||||||
await media.build();
|
|
||||||
const app = new MediaController(media).getController();
|
|
||||||
|
|
||||||
const file = Bun.file(`${import.meta.dir}/adapters/icon.png`);
|
async function makeApp(mediaOverride: Partial<TAppMediaConfig> = {}) {
|
||||||
console.log("file", file);
|
const app = createApp({
|
||||||
|
initialConfig: {
|
||||||
|
media: mergeObject(
|
||||||
|
{
|
||||||
|
enabled: true,
|
||||||
|
adapter: {
|
||||||
|
type: "local",
|
||||||
|
config: {
|
||||||
|
path: assetsTmpPath,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mediaOverride,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.build();
|
||||||
|
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ describe("Storage", async () => {
|
|||||||
|
|
||||||
test("uploads a file", async () => {
|
test("uploads a file", async () => {
|
||||||
const {
|
const {
|
||||||
meta: { type, size }
|
meta: { type, size },
|
||||||
} = await storage.uploadFile("hello", "world.txt");
|
} = await storage.uploadFile("hello", "world.txt");
|
||||||
expect({ type, size }).toEqual({ type: "text/plain", size: 0 });
|
expect({ type, size }).toEqual({ type: "text/plain", size: 0 });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ test("what", async () => {
|
|||||||
const mf = new Miniflare({
|
const mf = new Miniflare({
|
||||||
modules: true,
|
modules: true,
|
||||||
script: "export default { async fetch() { return new Response(null); } }",
|
script: "export default { async fetch() { return new Response(null); } }",
|
||||||
r2Buckets: ["BUCKET"]
|
r2Buckets: ["BUCKET"],
|
||||||
});
|
});
|
||||||
|
|
||||||
const bucket = await mf.getR2Bucket("BUCKET");
|
const bucket = await mf.getR2Bucket("BUCKET");
|
||||||
|
|||||||
@@ -8,17 +8,19 @@ const {
|
|||||||
CLOUDINARY_CLOUD_NAME,
|
CLOUDINARY_CLOUD_NAME,
|
||||||
CLOUDINARY_API_KEY,
|
CLOUDINARY_API_KEY,
|
||||||
CLOUDINARY_API_SECRET,
|
CLOUDINARY_API_SECRET,
|
||||||
CLOUDINARY_UPLOAD_PRESET
|
CLOUDINARY_UPLOAD_PRESET,
|
||||||
} = dotenvOutput.parsed!;
|
} = dotenvOutput.parsed!;
|
||||||
|
|
||||||
const ALL_TESTS = !!process.env.ALL_TESTS;
|
const ALL_TESTS = !!process.env.ALL_TESTS;
|
||||||
|
|
||||||
describe.skipIf(ALL_TESTS)("StorageCloudinaryAdapter", () => {
|
describe.skipIf(ALL_TESTS)("StorageCloudinaryAdapter", () => {
|
||||||
|
if (ALL_TESTS) return;
|
||||||
|
|
||||||
const adapter = new StorageCloudinaryAdapter({
|
const adapter = new StorageCloudinaryAdapter({
|
||||||
cloud_name: CLOUDINARY_CLOUD_NAME as string,
|
cloud_name: CLOUDINARY_CLOUD_NAME as string,
|
||||||
api_key: CLOUDINARY_API_KEY as string,
|
api_key: CLOUDINARY_API_KEY as string,
|
||||||
api_secret: CLOUDINARY_API_SECRET as string,
|
api_secret: CLOUDINARY_API_SECRET as string,
|
||||||
upload_preset: CLOUDINARY_UPLOAD_PRESET as string
|
upload_preset: CLOUDINARY_UPLOAD_PRESET as string,
|
||||||
});
|
});
|
||||||
|
|
||||||
const file = Bun.file(`${import.meta.dir}/icon.png`);
|
const file = Bun.file(`${import.meta.dir}/icon.png`);
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { randomString } from "../../../src/core/utils";
|
import { randomString } from "../../../src/core/utils";
|
||||||
import { StorageLocalAdapter } from "../../../src/media/storage/adapters/StorageLocalAdapter";
|
import { StorageLocalAdapter } from "../../../src/media/storage/adapters/StorageLocalAdapter";
|
||||||
|
import { assetsPath, assetsTmpPath } from "../../helper";
|
||||||
|
|
||||||
describe("StorageLocalAdapter", () => {
|
describe("StorageLocalAdapter", () => {
|
||||||
const adapter = new StorageLocalAdapter({
|
const adapter = new StorageLocalAdapter({
|
||||||
path: `${import.meta.dir}/local`
|
path: assetsTmpPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
const file = Bun.file(`${import.meta.dir}/icon.png`);
|
const file = Bun.file(`${assetsPath}/image.png`);
|
||||||
const _filename = randomString(10);
|
const _filename = randomString(10);
|
||||||
const filename = `${_filename}.png`;
|
const filename = `${_filename}.png`;
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ describe("StorageLocalAdapter", () => {
|
|||||||
|
|
||||||
test("puts an object", async () => {
|
test("puts an object", async () => {
|
||||||
objects = (await adapter.listObjects()).length;
|
objects = (await adapter.listObjects()).length;
|
||||||
expect(await adapter.putObject(filename, file)).toBeString();
|
expect(await adapter.putObject(filename, file as unknown as File)).toBeString();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("lists objects", async () => {
|
test("lists objects", async () => {
|
||||||
@@ -35,7 +36,7 @@ describe("StorageLocalAdapter", () => {
|
|||||||
test("gets object meta", async () => {
|
test("gets object meta", async () => {
|
||||||
expect(await adapter.getObjectMeta(filename)).toEqual({
|
expect(await adapter.getObjectMeta(filename)).toEqual({
|
||||||
type: file.type, // image/png
|
type: file.type, // image/png
|
||||||
size: file.size
|
size: file.size,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,26 +9,28 @@ const { R2_ACCESS_KEY, R2_SECRET_ACCESS_KEY, R2_URL, AWS_ACCESS_KEY, AWS_SECRET_
|
|||||||
|
|
||||||
// @todo: mock r2/s3 responses for faster tests
|
// @todo: mock r2/s3 responses for faster tests
|
||||||
const ALL_TESTS = !!process.env.ALL_TESTS;
|
const ALL_TESTS = !!process.env.ALL_TESTS;
|
||||||
|
console.log("ALL_TESTS?", ALL_TESTS);
|
||||||
|
|
||||||
|
describe.skipIf(true)("StorageS3Adapter", async () => {
|
||||||
|
if (ALL_TESTS) return;
|
||||||
|
|
||||||
describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => {
|
|
||||||
console.log("ALL_TESTS", process.env.ALL_TESTS);
|
|
||||||
const versions = [
|
const versions = [
|
||||||
[
|
[
|
||||||
"r2",
|
"r2",
|
||||||
new StorageS3Adapter({
|
new StorageS3Adapter({
|
||||||
access_key: R2_ACCESS_KEY as string,
|
access_key: R2_ACCESS_KEY as string,
|
||||||
secret_access_key: R2_SECRET_ACCESS_KEY as string,
|
secret_access_key: R2_SECRET_ACCESS_KEY as string,
|
||||||
url: R2_URL as string
|
url: R2_URL as string,
|
||||||
})
|
}),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"s3",
|
"s3",
|
||||||
new StorageS3Adapter({
|
new StorageS3Adapter({
|
||||||
access_key: AWS_ACCESS_KEY as string,
|
access_key: AWS_ACCESS_KEY as string,
|
||||||
secret_access_key: AWS_SECRET_KEY as string,
|
secret_access_key: AWS_SECRET_KEY as string,
|
||||||
url: AWS_S3_URL as string
|
url: AWS_S3_URL as string,
|
||||||
})
|
}),
|
||||||
]
|
],
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const _conf = {
|
const _conf = {
|
||||||
@@ -39,8 +41,8 @@ describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => {
|
|||||||
"objectExists",
|
"objectExists",
|
||||||
"getObject",
|
"getObject",
|
||||||
"deleteObject",
|
"deleteObject",
|
||||||
"getObjectMeta"
|
"getObjectMeta",
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const file = Bun.file(`${import.meta.dir}/icon.png`);
|
const file = Bun.file(`${import.meta.dir}/icon.png`);
|
||||||
@@ -55,7 +57,7 @@ describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => {
|
|||||||
|
|
||||||
// @todo: add mocked fetch for faster tests
|
// @todo: add mocked fetch for faster tests
|
||||||
describe.each(versions)("StorageS3Adapter for %s", async (name, adapter) => {
|
describe.each(versions)("StorageS3Adapter for %s", async (name, adapter) => {
|
||||||
if (!_conf.adapters.includes(name)) {
|
if (!_conf.adapters.includes(name) || ALL_TESTS) {
|
||||||
console.log("Skipping", name);
|
console.log("Skipping", name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -84,7 +86,7 @@ describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => {
|
|||||||
test.skipIf(disabled("getObjectMeta"))("gets object meta", async () => {
|
test.skipIf(disabled("getObjectMeta"))("gets object meta", async () => {
|
||||||
expect(await adapter.getObjectMeta(filename)).toEqual({
|
expect(await adapter.getObjectMeta(filename)).toEqual({
|
||||||
type: file.type, // image/png
|
type: file.type, // image/png
|
||||||
size: file.size
|
size: file.size,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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", () => {
|
||||||
@@ -27,11 +28,71 @@ 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"],
|
||||||
|
];
|
||||||
|
|
||||||
|
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"],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [filename, ext] of tests) {
|
||||||
|
expect(
|
||||||
|
getRandomizedFilename(filename).split(".").pop(),
|
||||||
|
`getRandomizedFilename(): ${filename} should end with ${ext}`,
|
||||||
|
).toBe(ext);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -43,10 +43,10 @@ describe("AppAuth", () => {
|
|||||||
{
|
{
|
||||||
enabled: true,
|
enabled: true,
|
||||||
jwt: {
|
jwt: {
|
||||||
secret: "123456"
|
secret: "123456",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
ctx
|
ctx,
|
||||||
);
|
);
|
||||||
|
|
||||||
await auth.build();
|
await auth.build();
|
||||||
@@ -63,12 +63,12 @@ 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);
|
||||||
@@ -85,10 +85,10 @@ describe("AppAuth", () => {
|
|||||||
auth: {
|
auth: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
jwt: {
|
jwt: {
|
||||||
secret: "123456"
|
secret: "123456",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await app.build();
|
await app.build();
|
||||||
@@ -109,14 +109,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 +132,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();
|
||||||
|
|
||||||
|
|||||||
@@ -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,10 +39,10 @@ describe("ModuleManager", async () => {
|
|||||||
basepath: "/api/data2",
|
basepath: "/api/data2",
|
||||||
entities: {
|
entities: {
|
||||||
test: entity("test", {
|
test: entity("test", {
|
||||||
content: text()
|
content: text(),
|
||||||
}).toJSON()
|
}).toJSON(),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
//const { version, ...json } = mm.toJSON() as any;
|
//const { version, ...json } = mm.toJSON() as any;
|
||||||
|
|
||||||
@@ -68,10 +69,10 @@ describe("ModuleManager", async () => {
|
|||||||
basepath: "/api/data2",
|
basepath: "/api/data2",
|
||||||
entities: {
|
entities: {
|
||||||
test: entity("test", {
|
test: entity("test", {
|
||||||
content: text()
|
content: text(),
|
||||||
}).toJSON()
|
}).toJSON(),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
//const { version, ...json } = mm.toJSON() as any;
|
//const { version, ...json } = mm.toJSON() as any;
|
||||||
|
|
||||||
@@ -104,7 +105,7 @@ describe("ModuleManager", async () => {
|
|||||||
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 }
|
initial: { version: version - 1, ...json },
|
||||||
});
|
});
|
||||||
await mm2.syncConfigTable();
|
await mm2.syncConfigTable();
|
||||||
|
|
||||||
@@ -128,7 +129,7 @@ describe("ModuleManager", async () => {
|
|||||||
const db = c2.dummyConnection.kysely;
|
const db = c2.dummyConnection.kysely;
|
||||||
|
|
||||||
const mm2 = new ModuleManager(c2.dummyConnection, {
|
const mm2 = new ModuleManager(c2.dummyConnection, {
|
||||||
initial: { version: version - 1, ...json }
|
initial: { version: version - 1, ...json },
|
||||||
});
|
});
|
||||||
await mm2.syncConfigTable();
|
await mm2.syncConfigTable();
|
||||||
await db
|
await db
|
||||||
@@ -156,8 +157,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)
|
||||||
@@ -177,7 +178,7 @@ describe("ModuleManager", async () => {
|
|||||||
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,9 +190,9 @@ describe("ModuleManager", async () => {
|
|||||||
...configs.server,
|
...configs.server,
|
||||||
admin: {
|
admin: {
|
||||||
...configs.server.admin,
|
...configs.server.admin,
|
||||||
color_scheme: "dark"
|
color_scheme: "dark",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -200,11 +201,11 @@ describe("ModuleManager", async () => {
|
|||||||
|
|
||||||
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();
|
||||||
|
|
||||||
@@ -226,9 +227,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 +237,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 +253,131 @@ 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 < 0) {
|
||||||
|
throw new Error("value must be positive");
|
||||||
|
}
|
||||||
|
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");
|
||||||
|
|
||||||
|
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,66 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { type InitialModuleConfigs, createApp } from "../../../src";
|
||||||
|
|
||||||
|
import type { Kysely } from "kysely";
|
||||||
|
import { getDummyConnection } from "../../helper";
|
||||||
|
import v7 from "./samples/v7.json";
|
||||||
|
|
||||||
|
// app expects migratable config to be present in database
|
||||||
|
async function createVersionedApp(config: InitialModuleConfigs) {
|
||||||
|
const { dummyConnection } = getDummyConnection();
|
||||||
|
|
||||||
|
if (!("version" in config)) throw new Error("config must have a version");
|
||||||
|
const { version, ...rest } = config;
|
||||||
|
|
||||||
|
const app = createApp({ connection: dummyConnection });
|
||||||
|
await app.build();
|
||||||
|
|
||||||
|
const qb = app.modules.ctx().connection.kysely as Kysely<any>;
|
||||||
|
const current = await qb
|
||||||
|
.selectFrom("__bknd")
|
||||||
|
.selectAll()
|
||||||
|
.where("type", "=", "config")
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
await qb
|
||||||
|
.updateTable("__bknd")
|
||||||
|
.set("json", JSON.stringify(rest))
|
||||||
|
.set("version", 7)
|
||||||
|
.where("id", "=", current!.id)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
const app2 = createApp({
|
||||||
|
connection: dummyConnection,
|
||||||
|
});
|
||||||
|
await app2.build();
|
||||||
|
return app2;
|
||||||
|
}
|
||||||
|
|
||||||
|
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()).toBe(8);
|
||||||
|
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();
|
||||||
|
expect(res.user.email).toBe("test@test.com");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it } from "bun:test";
|
|||||||
|
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { Guard } from "../../src/auth";
|
import { Guard } from "../../src/auth";
|
||||||
|
import { DebugLogger } from "../../src/core";
|
||||||
import { EventManager } from "../../src/core/events";
|
import { EventManager } from "../../src/core/events";
|
||||||
import { Default, stripMark } from "../../src/core/utils";
|
import { Default, stripMark } from "../../src/core/utils";
|
||||||
import { EntityManager } from "../../src/data";
|
import { EntityManager } from "../../src/data";
|
||||||
@@ -17,7 +18,8 @@ export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildCon
|
|||||||
emgr: new EventManager(),
|
emgr: new EventManager(),
|
||||||
guard: new Guard(),
|
guard: new Guard(),
|
||||||
flags: Module.ctx_flags,
|
flags: Module.ctx_flags,
|
||||||
...overrides
|
logger: new DebugLogger(false),
|
||||||
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { Draft2019 } from "json-schema-library";
|
||||||
|
import type { JSONSchema } from "json-schema-to-ts";
|
||||||
|
import * as utils from "../../src/ui/components/form/json-schema-form/utils";
|
||||||
|
import type { IsTypeType } from "../../src/ui/components/form/json-schema-form/utils";
|
||||||
|
|
||||||
|
describe("json form", () => {
|
||||||
|
test("coerse", () => {
|
||||||
|
const examples = [
|
||||||
|
["test", { type: "string" }, "test"],
|
||||||
|
["1", { type: "integer" }, 1],
|
||||||
|
["1", { type: "number" }, 1],
|
||||||
|
["true", { type: "boolean" }, true],
|
||||||
|
["false", { type: "boolean" }, false],
|
||||||
|
["1", { type: "boolean" }, true],
|
||||||
|
["0", { type: "boolean" }, false],
|
||||||
|
["on", { type: "boolean" }, true],
|
||||||
|
["off", { type: "boolean" }, false],
|
||||||
|
["null", { type: "null" }, null],
|
||||||
|
] satisfies [string, Exclude<JSONSchema, boolean>, any][];
|
||||||
|
|
||||||
|
for (const [input, schema, output] of examples) {
|
||||||
|
expect(utils.coerce(input, schema)).toBe(output);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("isType", () => {
|
||||||
|
const examples = [
|
||||||
|
["string", "string", true],
|
||||||
|
["integer", "number", false],
|
||||||
|
["number", "number", true],
|
||||||
|
["boolean", "boolean", true],
|
||||||
|
["null", "null", true],
|
||||||
|
["object", "object", true],
|
||||||
|
["array", "array", true],
|
||||||
|
["object", "array", false],
|
||||||
|
[["string", "number"], "number", true],
|
||||||
|
["number", ["string", "number"], true],
|
||||||
|
] satisfies [IsTypeType, IsTypeType, boolean][];
|
||||||
|
|
||||||
|
for (const [type, schemaType, output] of examples) {
|
||||||
|
expect(utils.isType(type, schemaType)).toBe(output);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getParentPointer", () => {
|
||||||
|
const examples = [
|
||||||
|
["#/nested/property/0/name", "#/nested/property/0"],
|
||||||
|
["#/nested/property/0", "#/nested/property"],
|
||||||
|
["#/nested/property", "#/nested"],
|
||||||
|
["#/nested", "#"],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [input, output] of examples) {
|
||||||
|
expect(utils.getParentPointer(input)).toBe(output);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("isRequired", () => {
|
||||||
|
const examples = [
|
||||||
|
[
|
||||||
|
"#/description",
|
||||||
|
{ type: "object", properties: { description: { type: "string" } } },
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"#/description",
|
||||||
|
{
|
||||||
|
type: "object",
|
||||||
|
required: ["description"],
|
||||||
|
properties: { description: { type: "string" } },
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"#/nested/property",
|
||||||
|
{
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
nested: {
|
||||||
|
type: "object",
|
||||||
|
properties: { property: { type: "string" } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"#/nested/property",
|
||||||
|
{
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
nested: {
|
||||||
|
type: "object",
|
||||||
|
required: ["property"],
|
||||||
|
properties: { property: { type: "string" } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
] satisfies [string, Exclude<JSONSchema, boolean>, boolean][];
|
||||||
|
|
||||||
|
for (const [pointer, schema, output] of examples) {
|
||||||
|
expect(utils.isRequired(new Draft2019(schema), pointer, schema)).toBe(output);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("prefixPath", () => {
|
||||||
|
const examples = [
|
||||||
|
["normal", "", "normal"],
|
||||||
|
["", "prefix", "prefix"],
|
||||||
|
["tags", "0", "0.tags"],
|
||||||
|
["tags", 0, "0.tags"],
|
||||||
|
["nested.property", "prefix", "prefix.nested.property"],
|
||||||
|
["nested.property", "", "nested.property"],
|
||||||
|
] satisfies [string, any, string][];
|
||||||
|
|
||||||
|
for (const [path, prefix, output] of examples) {
|
||||||
|
expect(utils.prefixPath(path, prefix)).toBe(output);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("suffixPath", () => {
|
||||||
|
const examples = [
|
||||||
|
["normal", "", "normal"],
|
||||||
|
["", "suffix", "suffix"],
|
||||||
|
["tags", "0", "tags.0"],
|
||||||
|
["tags", 0, "tags.0"],
|
||||||
|
["nested.property", "suffix", "nested.property.suffix"],
|
||||||
|
["nested.property", "", "nested.property"],
|
||||||
|
] satisfies [string, any, string][];
|
||||||
|
|
||||||
|
for (const [path, suffix, output] of examples) {
|
||||||
|
expect(utils.suffixPath(path, suffix)).toBe(output);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
+25
-25
@@ -19,11 +19,11 @@ const baseOptions: Partial<Omit<esbuild.BuildOptions, "plugins">> & { plugins?:
|
|||||||
format: "esm",
|
format: "esm",
|
||||||
drop: ["console", "debugger"],
|
drop: ["console", "debugger"],
|
||||||
loader: {
|
loader: {
|
||||||
".svg": "dataurl"
|
".svg": "dataurl",
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
__isDev: "0"
|
__isDev: "0",
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -40,7 +40,7 @@ const builds: Record<string, BuildFn> = {
|
|||||||
"src/core/index.ts",
|
"src/core/index.ts",
|
||||||
"src/core/utils/index.ts",
|
"src/core/utils/index.ts",
|
||||||
"src/ui/index.ts",
|
"src/ui/index.ts",
|
||||||
"src/ui/main.css"
|
"src/ui/main.css",
|
||||||
],
|
],
|
||||||
outdir: "dist",
|
outdir: "dist",
|
||||||
outExtension: { ".js": format === "esm" ? ".js" : ".cjs" },
|
outExtension: { ".js": format === "esm" ? ".js" : ".cjs" },
|
||||||
@@ -49,7 +49,7 @@ const builds: Record<string, BuildFn> = {
|
|||||||
bundle: true,
|
bundle: true,
|
||||||
plugins: [postcss()],
|
plugins: [postcss()],
|
||||||
//target: "es2022",
|
//target: "es2022",
|
||||||
format
|
format,
|
||||||
}),
|
}),
|
||||||
/*components: (format = "esm") => ({
|
/*components: (format = "esm") => ({
|
||||||
...baseOptions,
|
...baseOptions,
|
||||||
@@ -84,11 +84,11 @@ const builds: Record<string, BuildFn> = {
|
|||||||
format,
|
format,
|
||||||
loader: {
|
loader: {
|
||||||
".svg": "dataurl",
|
".svg": "dataurl",
|
||||||
".js": "jsx"
|
".js": "jsx",
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
__isDev: "0",
|
__isDev: "0",
|
||||||
"process.env.NODE_ENV": '"production"'
|
"process.env.NODE_ENV": '"production"',
|
||||||
},
|
},
|
||||||
chunkNames: "chunks/[name]-[hash]",
|
chunkNames: "chunks/[name]-[hash]",
|
||||||
plugins: [
|
plugins: [
|
||||||
@@ -100,7 +100,7 @@ const builds: Record<string, BuildFn> = {
|
|||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
path: output,
|
path: output,
|
||||||
mime: guessMimeType(name)
|
mime: guessMimeType(name),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
for (const { output, meta } of info) {
|
for (const { output, meta } of info) {
|
||||||
@@ -113,9 +113,9 @@ const builds: Record<string, BuildFn> = {
|
|||||||
const manifest_file = "dist/static/manifest.json";
|
const manifest_file = "dist/static/manifest.json";
|
||||||
await Bun.write(manifest_file, JSON.stringify(manifest, null, 2));
|
await Bun.write(manifest_file, JSON.stringify(manifest, null, 2));
|
||||||
console.log(`Manifest written to ${manifest_file}`, manifest);
|
console.log(`Manifest written to ${manifest_file}`, manifest);
|
||||||
})
|
}),
|
||||||
]
|
],
|
||||||
})
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
function adapter(adapter: string, overrides: Partial<esbuild.BuildOptions> = {}): BuildOptions {
|
function adapter(adapter: string, overrides: Partial<esbuild.BuildOptions> = {}): BuildOptions {
|
||||||
@@ -135,12 +135,12 @@ function adapter(adapter: string, overrides: Partial<esbuild.BuildOptions> = {})
|
|||||||
"react*",
|
"react*",
|
||||||
"next*",
|
"next*",
|
||||||
"libsql",
|
"libsql",
|
||||||
"@libsql*"
|
"@libsql*",
|
||||||
],
|
],
|
||||||
splitting: false,
|
splitting: false,
|
||||||
treeShaking: true,
|
treeShaking: true,
|
||||||
bundle: true,
|
bundle: true,
|
||||||
...overrides
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const adapters = [
|
const adapters = [
|
||||||
@@ -152,7 +152,7 @@ const adapters = [
|
|||||||
adapter("remix", { format: "cjs" }),
|
adapter("remix", { format: "cjs" }),
|
||||||
adapter("bun"),
|
adapter("bun"),
|
||||||
adapter("node", { platform: "node", format: "esm" }),
|
adapter("node", { platform: "node", format: "esm" }),
|
||||||
adapter("node", { platform: "node", format: "cjs" })
|
adapter("node", { platform: "node", format: "cjs" }),
|
||||||
];
|
];
|
||||||
|
|
||||||
const collect = [
|
const collect = [
|
||||||
@@ -161,7 +161,7 @@ const collect = [
|
|||||||
//builds.components(),
|
//builds.components(),
|
||||||
builds.backend("cjs"),
|
builds.backend("cjs"),
|
||||||
//builds.components("cjs"),
|
//builds.components("cjs"),
|
||||||
...adapters
|
...adapters,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (watch) {
|
if (watch) {
|
||||||
@@ -172,7 +172,7 @@ if (watch) {
|
|||||||
} = {
|
} = {
|
||||||
timeout: undefined,
|
timeout: undefined,
|
||||||
cleanup: undefined,
|
cleanup: undefined,
|
||||||
building: undefined
|
building: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
async function rebuildTypes() {
|
async function rebuildTypes() {
|
||||||
@@ -190,9 +190,9 @@ if (watch) {
|
|||||||
onExit: () => {
|
onExit: () => {
|
||||||
_state.building = undefined;
|
_state.building = undefined;
|
||||||
console.log("Types rebuilt");
|
console.log("Types rebuilt");
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
@@ -209,9 +209,9 @@ if (watch) {
|
|||||||
console.log(`rebuilt ${name} with ${result.errors.length} errors`);
|
console.log(`rebuilt ${name} with ${result.errors.length} errors`);
|
||||||
rebuildTypes();
|
rebuildTypes();
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
ctx.watch();
|
ctx.watch();
|
||||||
}
|
}
|
||||||
@@ -235,9 +235,9 @@ if (watch) {
|
|||||||
const from = String(i).padStart(String(count).length);
|
const from = String(i).padStart(String(count).length);
|
||||||
console.log(`[${from}/${count}] built ${name} with ${errors} errors`);
|
console.log(`[${from}/${count}] built ${name} with ${errors} errors`);
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,7 +249,7 @@ if (watch) {
|
|||||||
Bun.spawn(["bun", "build:types"], {
|
Bun.spawn(["bun", "build:types"], {
|
||||||
onExit: () => {
|
onExit: () => {
|
||||||
console.log("Types rebuilt");
|
console.log("Types rebuilt");
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+161
-117
@@ -27,9 +27,9 @@ function buildTypes() {
|
|||||||
onExit: () => {
|
onExit: () => {
|
||||||
console.log("Types aliased");
|
console.log("Types aliased");
|
||||||
types_running = false;
|
types_running = false;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,111 +49,133 @@ if (types && !watch) {
|
|||||||
/**
|
/**
|
||||||
* Building backend and general API
|
* Building backend and general API
|
||||||
*/
|
*/
|
||||||
await tsup.build({
|
async function buildApi() {
|
||||||
minify,
|
await tsup.build({
|
||||||
sourcemap,
|
minify,
|
||||||
watch,
|
sourcemap,
|
||||||
entry: ["src/index.ts", "src/data/index.ts", "src/core/index.ts", "src/core/utils/index.ts"],
|
watch,
|
||||||
outDir: "dist",
|
entry: ["src/index.ts", "src/data/index.ts", "src/core/index.ts", "src/core/utils/index.ts"],
|
||||||
external: ["bun:test", "@libsql/client"],
|
outDir: "dist",
|
||||||
metafile: true,
|
external: ["bun:test", "@libsql/client"],
|
||||||
platform: "browser",
|
metafile: true,
|
||||||
format: ["esm"],
|
platform: "browser",
|
||||||
splitting: false,
|
format: ["esm"],
|
||||||
treeshake: true,
|
splitting: false,
|
||||||
loader: {
|
treeshake: true,
|
||||||
".svg": "dataurl"
|
loader: {
|
||||||
},
|
".svg": "dataurl",
|
||||||
onSuccess: async () => {
|
},
|
||||||
delayTypes();
|
onSuccess: async () => {
|
||||||
}
|
delayTypes();
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rewriteClient(path: string) {
|
||||||
|
const bundle = await Bun.file(path).text();
|
||||||
|
await Bun.write(path, '"use client";\n' + bundle.replaceAll("ui/client", "bknd/client"));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Building UI for direct imports
|
* Building UI for direct imports
|
||||||
*/
|
*/
|
||||||
await tsup.build({
|
async function buildUi() {
|
||||||
minify,
|
const base = {
|
||||||
sourcemap,
|
minify,
|
||||||
watch,
|
sourcemap,
|
||||||
entry: ["src/ui/index.ts", "src/ui/client/index.ts", "src/ui/main.css", "src/ui/styles.css"],
|
watch,
|
||||||
outDir: "dist/ui",
|
external: [
|
||||||
external: [
|
"bun:test",
|
||||||
"bun:test",
|
"react",
|
||||||
"react",
|
"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",
|
/codemirror/,
|
||||||
/codemirror/,
|
"@xyflow/react",
|
||||||
"@xyflow/react",
|
"@mantine/core",
|
||||||
"@mantine/core"
|
],
|
||||||
],
|
metafile: true,
|
||||||
metafile: true,
|
platform: "browser",
|
||||||
platform: "browser",
|
format: ["esm"],
|
||||||
format: ["esm"],
|
splitting: false,
|
||||||
splitting: false,
|
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;
|
||||||
onSuccess: async () => {
|
|
||||||
delayTypes();
|
await tsup.build({
|
||||||
}
|
...base,
|
||||||
});
|
entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"],
|
||||||
|
outDir: "dist/ui",
|
||||||
|
onSuccess: async () => {
|
||||||
|
await rewriteClient("./dist/ui/index.js");
|
||||||
|
delayTypes();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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() {
|
||||||
minify,
|
await tsup.build({
|
||||||
sourcemap,
|
minify,
|
||||||
watch,
|
sourcemap,
|
||||||
entry: ["src/ui/elements/index.ts"],
|
watch,
|
||||||
outDir: "dist/ui/elements",
|
entry: ["src/ui/elements/index.ts"],
|
||||||
external: [
|
outDir: "dist/ui/elements",
|
||||||
"ui/client",
|
external: [
|
||||||
"react",
|
"ui/client",
|
||||||
"react-dom",
|
"react",
|
||||||
"react/jsx-runtime",
|
"react-dom",
|
||||||
"react/jsx-dev-runtime",
|
"react/jsx-runtime",
|
||||||
"use-sync-external-store"
|
"react/jsx-dev-runtime",
|
||||||
],
|
"use-sync-external-store",
|
||||||
metafile: true,
|
],
|
||||||
platform: "browser",
|
metafile: true,
|
||||||
format: ["esm"],
|
platform: "browser",
|
||||||
splitting: false,
|
format: ["esm"],
|
||||||
bundle: true,
|
splitting: false,
|
||||||
treeshake: true,
|
bundle: true,
|
||||||
loader: {
|
treeshake: true,
|
||||||
".svg": "dataurl"
|
loader: {
|
||||||
},
|
".svg": "dataurl",
|
||||||
esbuildOptions: (options) => {
|
},
|
||||||
options.alias = {
|
esbuildOptions: (options) => {
|
||||||
// not important for elements, mock to reduce bundle
|
options.alias = {
|
||||||
"tailwind-merge": "./src/ui/elements/mocks/tailwind-merge.ts"
|
// not important for elements, mock to reduce bundle
|
||||||
};
|
"tailwind-merge": "./src/ui/elements/mocks/tailwind-merge.ts",
|
||||||
},
|
};
|
||||||
onSuccess: async () => {
|
},
|
||||||
// manually replace ui/client with bknd/client
|
onSuccess: async () => {
|
||||||
const path = "./dist/ui/elements/index.js";
|
await rewriteClient("./dist/ui/elements/index.js");
|
||||||
const bundle = await Bun.file(path).text();
|
delayTypes();
|
||||||
await Bun.write(path, bundle.replaceAll("ui/client", "bknd/client"));
|
},
|
||||||
|
});
|
||||||
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,40 +184,62 @@ 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,
|
|
||||||
treeshake: true,
|
|
||||||
onSuccess: async () => {
|
|
||||||
delayTypes();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await tsup.build(baseConfig("remix"));
|
async function buildAdapters() {
|
||||||
await tsup.build(baseConfig("bun"));
|
// base adapter handles
|
||||||
await tsup.build(baseConfig("astro"));
|
await tsup.build({
|
||||||
await tsup.build(baseConfig("cloudflare"));
|
...baseConfig(""),
|
||||||
|
entry: ["src/adapter/index.ts"],
|
||||||
|
outDir: "dist/adapter",
|
||||||
|
});
|
||||||
|
|
||||||
await tsup.build({
|
// specific adatpers
|
||||||
...baseConfig("vite"),
|
await tsup.build(baseConfig("remix"));
|
||||||
platform: "node"
|
await tsup.build(baseConfig("bun"));
|
||||||
});
|
await tsup.build(baseConfig("astro"));
|
||||||
|
await tsup.build(baseConfig("aws"));
|
||||||
|
await tsup.build(
|
||||||
|
baseConfig("cloudflare", {
|
||||||
|
external: [/^kysely/],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
await tsup.build({
|
await tsup.build({
|
||||||
...baseConfig("nextjs"),
|
...baseConfig("vite"),
|
||||||
platform: "node"
|
platform: "node",
|
||||||
});
|
});
|
||||||
|
|
||||||
await tsup.build({
|
await tsup.build({
|
||||||
...baseConfig("node"),
|
...baseConfig("nextjs"),
|
||||||
platform: "node"
|
platform: "node",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await tsup.build({
|
||||||
|
...baseConfig("node"),
|
||||||
|
platform: "node",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await buildApi();
|
||||||
|
await buildUi();
|
||||||
|
await buildUiElements();
|
||||||
|
await buildAdapters();
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
+33
-15
@@ -3,7 +3,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"bin": "./dist/cli/index.js",
|
"bin": "./dist/cli/index.js",
|
||||||
"version": "0.6.1",
|
"version": "0.9.1",
|
||||||
"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, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
|
||||||
"homepage": "https://bknd.io",
|
"homepage": "https://bknd.io",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -22,9 +22,9 @@
|
|||||||
"build:cli": "bun build src/cli/index.ts --target node --outdir dist/cli --minify",
|
"build:cli": "bun build src/cli/index.ts --target node --outdir dist/cli --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 build:all && cp ../README.md ./",
|
||||||
@@ -33,37 +33,44 @@
|
|||||||
"license": "FSL-1.1-MIT",
|
"license": "FSL-1.1-MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cfworker/json-schema": "^2.0.1",
|
"@cfworker/json-schema": "^2.0.1",
|
||||||
|
"@codemirror/lang-html": "^6.4.9",
|
||||||
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
|
"@codemirror/lang-liquid": "^6.2.1",
|
||||||
|
"@hello-pangea/dnd": "^17.0.0",
|
||||||
"@libsql/client": "^0.14.0",
|
"@libsql/client": "^0.14.0",
|
||||||
|
"@mantine/core": "^7.13.4",
|
||||||
"@sinclair/typebox": "^0.32.34",
|
"@sinclair/typebox": "^0.32.34",
|
||||||
"@tanstack/react-form": "0.19.2",
|
"@tanstack/react-form": "0.19.2",
|
||||||
|
"@uiw/react-codemirror": "^4.23.6",
|
||||||
|
"@xyflow/react": "^12.3.2",
|
||||||
"aws4fetch": "^1.0.18",
|
"aws4fetch": "^1.0.18",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"fast-xml-parser": "^4.4.0",
|
"fast-xml-parser": "^4.4.0",
|
||||||
"hono": "^4.6.12",
|
"hono": "^4.6.12",
|
||||||
|
"json-schema-form-react": "^0.0.2",
|
||||||
|
"json-schema-library": "^10.0.0-rc7",
|
||||||
|
"json-schema-to-ts": "^3.1.1",
|
||||||
"kysely": "^0.27.4",
|
"kysely": "^0.27.4",
|
||||||
"liquidjs": "^10.15.0",
|
"liquidjs": "^10.15.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"oauth4webapi": "^2.11.1",
|
"oauth4webapi": "^2.11.1",
|
||||||
"swr": "^2.2.5",
|
"object-path-immutable": "^4.1.2",
|
||||||
"json-schema-form-react": "^0.0.2",
|
"picocolors": "^1.1.1",
|
||||||
"@uiw/react-codemirror": "^4.23.6",
|
"radix-ui": "^1.1.2",
|
||||||
"@codemirror/lang-html": "^6.4.9",
|
"swr": "^2.2.5"
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
|
||||||
"@codemirror/lang-liquid": "^6.2.1",
|
|
||||||
"@xyflow/react": "^12.3.2",
|
|
||||||
"@mantine/core": "^7.13.4",
|
|
||||||
"@hello-pangea/dnd": "^17.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.613.0",
|
"@aws-sdk/client-s3": "^3.613.0",
|
||||||
|
"@bluwy/giget-core": "^0.1.2",
|
||||||
"@dagrejs/dagre": "^1.1.4",
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
|
"@mantine/modals": "^7.13.4",
|
||||||
|
"@mantine/notifications": "^7.13.4",
|
||||||
"@hono/typebox-validator": "^0.2.6",
|
"@hono/typebox-validator": "^0.2.6",
|
||||||
"@hono/vite-dev-server": "^0.17.0",
|
"@hono/vite-dev-server": "^0.17.0",
|
||||||
"@hono/zod-validator": "^0.4.1",
|
"@hono/zod-validator": "^0.4.1",
|
||||||
"@hookform/resolvers": "^3.9.1",
|
"@hookform/resolvers": "^3.9.1",
|
||||||
"@libsql/kysely-libsql": "^0.4.1",
|
"@libsql/kysely-libsql": "^0.4.1",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.0",
|
"@rjsf/core": "5.22.2",
|
||||||
"@rjsf/core": "^5.22.2",
|
|
||||||
"@tabler/icons-react": "3.18.0",
|
"@tabler/icons-react": "3.18.0",
|
||||||
"@types/node": "^22.10.0",
|
"@types/node": "^22.10.0",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
@@ -71,8 +78,10 @@
|
|||||||
"@vitejs/plugin-react": "^4.3.3",
|
"@vitejs/plugin-react": "^4.3.3",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"dotenv": "^16.4.7",
|
||||||
"esbuild-postcss": "^0.0.4",
|
"esbuild-postcss": "^0.0.4",
|
||||||
"jotai": "^2.10.1",
|
"jotai": "^2.10.1",
|
||||||
|
"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.4.47",
|
||||||
@@ -148,6 +157,10 @@
|
|||||||
"import": "./dist/adapter/cloudflare/index.js",
|
"import": "./dist/adapter/cloudflare/index.js",
|
||||||
"require": "./dist/adapter/cloudflare/index.cjs"
|
"require": "./dist/adapter/cloudflare/index.cjs"
|
||||||
},
|
},
|
||||||
|
"./adapter": {
|
||||||
|
"types": "./dist/types/adapter/index.d.ts",
|
||||||
|
"import": "./dist/adapter/index.js"
|
||||||
|
},
|
||||||
"./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",
|
||||||
@@ -178,6 +191,11 @@
|
|||||||
"import": "./dist/adapter/astro/index.js",
|
"import": "./dist/adapter/astro/index.js",
|
||||||
"require": "./dist/adapter/astro/index.cjs"
|
"require": "./dist/adapter/astro/index.cjs"
|
||||||
},
|
},
|
||||||
|
"./adapter/aws": {
|
||||||
|
"types": "./dist/types/adapter/aws/index.d.ts",
|
||||||
|
"import": "./dist/adapter/aws/index.js",
|
||||||
|
"require": "./dist/adapter/aws/index.cjs"
|
||||||
|
},
|
||||||
"./dist/main.css": "./dist/ui/main.css",
|
"./dist/main.css": "./dist/ui/main.css",
|
||||||
"./dist/styles.css": "./dist/ui/styles.css",
|
"./dist/styles.css": "./dist/ui/styles.css",
|
||||||
"./dist/manifest.json": "./dist/static/.vite/manifest.json"
|
"./dist/manifest.json": "./dist/static/.vite/manifest.json"
|
||||||
@@ -210,4 +228,4 @@
|
|||||||
"bun",
|
"bun",
|
||||||
"node"
|
"node"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -11,8 +11,8 @@ export default {
|
|||||||
"mantine-breakpoint-sm": "48em",
|
"mantine-breakpoint-sm": "48em",
|
||||||
"mantine-breakpoint-md": "62em",
|
"mantine-breakpoint-md": "62em",
|
||||||
"mantine-breakpoint-lg": "75em",
|
"mantine-breakpoint-lg": "75em",
|
||||||
"mantine-breakpoint-xl": "88em"
|
"mantine-breakpoint-xl": "88em",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+76
-31
@@ -2,9 +2,9 @@ import type { SafeUser } from "auth";
|
|||||||
import { AuthApi } from "auth/api/AuthApi";
|
import { AuthApi } from "auth/api/AuthApi";
|
||||||
import { DataApi } from "data/api/DataApi";
|
import { DataApi } from "data/api/DataApi";
|
||||||
import { decode } from "hono/jwt";
|
import { decode } from "hono/jwt";
|
||||||
import { omit } from "lodash-es";
|
|
||||||
import { MediaApi } from "media/api/MediaApi";
|
import { MediaApi } from "media/api/MediaApi";
|
||||||
import { SystemApi } from "modules/SystemApi";
|
import { SystemApi } from "modules/SystemApi";
|
||||||
|
import { omitKeys } from "core/utils";
|
||||||
|
|
||||||
export type TApiUser = SafeUser;
|
export type TApiUser = SafeUser;
|
||||||
|
|
||||||
@@ -17,13 +17,22 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ApiOptions = {
|
export type ApiOptions = {
|
||||||
host: string;
|
host?: string;
|
||||||
user?: TApiUser;
|
|
||||||
token?: string;
|
|
||||||
headers?: Headers;
|
headers?: Headers;
|
||||||
key?: string;
|
key?: string;
|
||||||
localStorage?: boolean;
|
localStorage?: boolean;
|
||||||
};
|
fetcher?: typeof fetch;
|
||||||
|
verbose?: boolean;
|
||||||
|
verified?: boolean;
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
token?: string;
|
||||||
|
user?: TApiUser;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
request: Request;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export type AuthState = {
|
export type AuthState = {
|
||||||
token?: string;
|
token?: string;
|
||||||
@@ -42,14 +51,26 @@ export class Api {
|
|||||||
public auth!: AuthApi;
|
public auth!: AuthApi;
|
||||||
public media!: MediaApi;
|
public media!: MediaApi;
|
||||||
|
|
||||||
constructor(private readonly options: ApiOptions) {
|
constructor(private options: ApiOptions = {}) {
|
||||||
if (options.user) {
|
// only mark verified if forced
|
||||||
this.user = options.user;
|
this.verified = options.verified === true;
|
||||||
this.token_transport = "none";
|
|
||||||
this.verified = true;
|
// prefer request if given
|
||||||
} else if (options.token) {
|
if ("request" in options) {
|
||||||
|
this.options.host = options.host ?? new URL(options.request.url).origin;
|
||||||
|
this.options.headers = options.headers ?? options.request.headers;
|
||||||
|
this.extractToken();
|
||||||
|
|
||||||
|
// then check for a token
|
||||||
|
} else if ("token" in options) {
|
||||||
this.token_transport = "header";
|
this.token_transport = "header";
|
||||||
this.updateToken(options.token);
|
this.updateToken(options.token);
|
||||||
|
|
||||||
|
// then check for an user object
|
||||||
|
} else if ("user" in options) {
|
||||||
|
this.token_transport = "none";
|
||||||
|
this.user = options.user;
|
||||||
|
this.verified = options.verified !== false;
|
||||||
} else {
|
} else {
|
||||||
this.extractToken();
|
this.extractToken();
|
||||||
}
|
}
|
||||||
@@ -58,7 +79,7 @@ export class Api {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get baseUrl() {
|
get baseUrl() {
|
||||||
return this.options.host;
|
return this.options.host ?? "http://localhost";
|
||||||
}
|
}
|
||||||
|
|
||||||
get tokenKey() {
|
get tokenKey() {
|
||||||
@@ -66,13 +87,15 @@ export class Api {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private extractToken() {
|
private extractToken() {
|
||||||
|
// if token has to be extracted, it's never verified
|
||||||
|
this.verified = false;
|
||||||
|
|
||||||
if (this.options.headers) {
|
if (this.options.headers) {
|
||||||
// try cookies
|
// try cookies
|
||||||
const cookieToken = getCookieValue(this.options.headers.get("cookie"), "auth");
|
const cookieToken = getCookieValue(this.options.headers.get("cookie"), "auth");
|
||||||
if (cookieToken) {
|
if (cookieToken) {
|
||||||
this.updateToken(cookieToken);
|
|
||||||
this.token_transport = "cookie";
|
this.token_transport = "cookie";
|
||||||
this.verified = true;
|
this.updateToken(cookieToken);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,8 +119,10 @@ export class Api {
|
|||||||
|
|
||||||
updateToken(token?: string, rebuild?: boolean) {
|
updateToken(token?: string, rebuild?: boolean) {
|
||||||
this.token = token;
|
this.token = token;
|
||||||
|
this.verified = false;
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
this.user = omit(decode(token).payload as any, ["iat", "iss", "exp"]) as any;
|
this.user = omitKeys(decode(token).payload as any, ["iat", "iss", "exp"]) as any;
|
||||||
} else {
|
} else {
|
||||||
this.user = undefined;
|
this.user = undefined;
|
||||||
}
|
}
|
||||||
@@ -115,19 +140,28 @@ export class Api {
|
|||||||
if (rebuild) this.buildApis();
|
if (rebuild) this.buildApis();
|
||||||
}
|
}
|
||||||
|
|
||||||
markAuthVerified(verfied: boolean) {
|
private markAuthVerified(verfied: boolean) {
|
||||||
this.verified = verfied;
|
this.verified = verfied;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isAuthVerified(): boolean {
|
||||||
|
return this.verified;
|
||||||
|
}
|
||||||
|
|
||||||
getAuthState(): AuthState {
|
getAuthState(): AuthState {
|
||||||
return {
|
return {
|
||||||
token: this.token,
|
token: this.token,
|
||||||
user: this.user,
|
user: this.user,
|
||||||
verified: this.verified
|
verified: this.verified,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isAuthenticated(): boolean {
|
||||||
|
const { token, user } = this.getAuthState();
|
||||||
|
return !!token && !!user;
|
||||||
|
}
|
||||||
|
|
||||||
async getVerifiedAuthState(): Promise<AuthState> {
|
async getVerifiedAuthState(): Promise<AuthState> {
|
||||||
await this.verifyAuth();
|
await this.verifyAuth();
|
||||||
return this.getAuthState();
|
return this.getAuthState();
|
||||||
@@ -140,11 +174,13 @@ export class Api {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await this.auth.me();
|
const { ok, data } = await this.auth.me();
|
||||||
if (!res.ok || !res.body.user) {
|
const user = data?.user;
|
||||||
|
if (!ok || !user) {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.user = user;
|
||||||
this.markAuthVerified(true);
|
this.markAuthVerified(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.markAuthVerified(false);
|
this.markAuthVerified(false);
|
||||||
@@ -156,21 +192,30 @@ export class Api {
|
|||||||
return this.user || null;
|
return this.user || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildApis() {
|
getParams() {
|
||||||
const baseParams = {
|
return Object.freeze({
|
||||||
host: this.options.host,
|
host: this.baseUrl,
|
||||||
token: this.token,
|
token: this.token,
|
||||||
headers: this.options.headers,
|
headers: this.options.headers,
|
||||||
token_transport: this.token_transport
|
token_transport: this.token_transport,
|
||||||
};
|
verbose: this.options.verbose,
|
||||||
|
|
||||||
this.system = new SystemApi(baseParams);
|
|
||||||
this.data = new DataApi(baseParams);
|
|
||||||
this.auth = new AuthApi({
|
|
||||||
...baseParams,
|
|
||||||
onTokenUpdate: (token) => this.updateToken(token, true)
|
|
||||||
});
|
});
|
||||||
this.media = new MediaApi(baseParams);
|
}
|
||||||
|
|
||||||
|
private buildApis() {
|
||||||
|
const baseParams = this.getParams();
|
||||||
|
const fetcher = this.options.fetcher;
|
||||||
|
|
||||||
|
this.system = new SystemApi(baseParams, fetcher);
|
||||||
|
this.data = new DataApi(baseParams, fetcher);
|
||||||
|
this.auth = new AuthApi(
|
||||||
|
{
|
||||||
|
...baseParams,
|
||||||
|
onTokenUpdate: (token) => this.updateToken(token, true),
|
||||||
|
},
|
||||||
|
fetcher,
|
||||||
|
);
|
||||||
|
this.media = new MediaApi(baseParams, fetcher);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+81
-34
@@ -1,16 +1,22 @@
|
|||||||
import type { CreateUserPayload } from "auth/AppAuth";
|
import type { CreateUserPayload } from "auth/AppAuth";
|
||||||
|
import { $console } from "core";
|
||||||
import { Event } from "core/events";
|
import { Event } from "core/events";
|
||||||
import { Connection, type LibSqlCredentials, LibsqlConnection } from "data";
|
import { Connection, type LibSqlCredentials, LibsqlConnection } from "data";
|
||||||
|
import type { Hono } from "hono";
|
||||||
import {
|
import {
|
||||||
type InitialModuleConfigs,
|
type InitialModuleConfigs,
|
||||||
|
type ModuleBuildContext,
|
||||||
ModuleManager,
|
ModuleManager,
|
||||||
type ModuleManagerOptions,
|
type ModuleManagerOptions,
|
||||||
type Modules
|
type Modules,
|
||||||
} from "modules/ModuleManager";
|
} from "modules/ModuleManager";
|
||||||
import * as SystemPermissions from "modules/permissions";
|
import * as SystemPermissions from "modules/permissions";
|
||||||
import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
|
import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
|
||||||
import { SystemController } from "modules/server/SystemController";
|
import { SystemController } from "modules/server/SystemController";
|
||||||
|
|
||||||
|
// biome-ignore format: must be there
|
||||||
|
import { Api, type ApiOptions } from "Api";
|
||||||
|
|
||||||
export type AppPlugin = (app: App) => Promise<void> | void;
|
export type AppPlugin = (app: App) => Promise<void> | void;
|
||||||
|
|
||||||
abstract class AppEvent<A = {}> extends Event<{ app: App } & A> {}
|
abstract class AppEvent<A = {}> extends Event<{ app: App } & A> {}
|
||||||
@@ -25,57 +31,74 @@ export class AppFirstBoot extends AppEvent {
|
|||||||
}
|
}
|
||||||
export const AppEvents = { AppConfigUpdatedEvent, AppBuiltEvent, AppFirstBoot } as const;
|
export const AppEvents = { AppConfigUpdatedEvent, AppBuiltEvent, AppFirstBoot } as const;
|
||||||
|
|
||||||
|
export type AppOptions = {
|
||||||
|
plugins?: AppPlugin[];
|
||||||
|
seed?: (ctx: ModuleBuildContext & { app: App }) => Promise<void>;
|
||||||
|
manager?: Omit<ModuleManagerOptions, "initial" | "onUpdated" | "seed">;
|
||||||
|
};
|
||||||
export type CreateAppConfig = {
|
export type CreateAppConfig = {
|
||||||
connection?:
|
connection?:
|
||||||
| Connection
|
| Connection
|
||||||
| {
|
| {
|
||||||
|
// @deprecated
|
||||||
type: "libsql";
|
type: "libsql";
|
||||||
config: LibSqlCredentials;
|
config: LibSqlCredentials;
|
||||||
};
|
}
|
||||||
|
| LibSqlCredentials;
|
||||||
initialConfig?: InitialModuleConfigs;
|
initialConfig?: InitialModuleConfigs;
|
||||||
plugins?: AppPlugin[];
|
options?: AppOptions;
|
||||||
options?: Omit<ModuleManagerOptions, "initial" | "onUpdated">;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AppConfig = InitialModuleConfigs;
|
export type AppConfig = InitialModuleConfigs;
|
||||||
|
export type LocalApiOptions = Request | ApiOptions;
|
||||||
|
|
||||||
export class App {
|
export class App {
|
||||||
modules: ModuleManager;
|
modules: ModuleManager;
|
||||||
static readonly Events = AppEvents;
|
static readonly Events = AppEvents;
|
||||||
adminController?: AdminController;
|
adminController?: AdminController;
|
||||||
private trigger_first_boot = false;
|
private trigger_first_boot = false;
|
||||||
|
private plugins: AppPlugin[];
|
||||||
|
private _id: string = crypto.randomUUID();
|
||||||
|
private _building: boolean = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private connection: Connection,
|
private connection: Connection,
|
||||||
_initialConfig?: InitialModuleConfigs,
|
_initialConfig?: InitialModuleConfigs,
|
||||||
private plugins: AppPlugin[] = [],
|
private options?: AppOptions,
|
||||||
moduleManagerOptions?: ModuleManagerOptions
|
|
||||||
) {
|
) {
|
||||||
|
this.plugins = options?.plugins ?? [];
|
||||||
this.modules = new ModuleManager(connection, {
|
this.modules = new ModuleManager(connection, {
|
||||||
...moduleManagerOptions,
|
...(options?.manager ?? {}),
|
||||||
initial: _initialConfig,
|
initial: _initialConfig,
|
||||||
onUpdated: async (key, config) => {
|
onUpdated: async (key, config) => {
|
||||||
// if the EventManager was disabled, we assume we shouldn't
|
// if the EventManager was disabled, we assume we shouldn't
|
||||||
// respond to events, such as "onUpdated".
|
// respond to events, such as "onUpdated".
|
||||||
|
// this is important if multiple changes are done, and then build() is called manually
|
||||||
if (!this.emgr.enabled) {
|
if (!this.emgr.enabled) {
|
||||||
console.warn("[APP] config updated, but event manager is disabled, skip.");
|
$console.warn("App config updated, but event manager is disabled, skip.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[APP] config updated", key);
|
$console.log("App config updated", key);
|
||||||
await this.build({ sync: true, save: true });
|
// @todo: potentially double syncing
|
||||||
|
await this.build({ sync: true });
|
||||||
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this }));
|
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this }));
|
||||||
},
|
},
|
||||||
onFirstBoot: async () => {
|
onFirstBoot: async () => {
|
||||||
console.log("[APP] first boot");
|
$console.log("App first boot");
|
||||||
this.trigger_first_boot = true;
|
this.trigger_first_boot = true;
|
||||||
},
|
},
|
||||||
onServerInit: async (server) => {
|
onServerInit: async (server) => {
|
||||||
server.use(async (c, next) => {
|
server.use(async (c, next) => {
|
||||||
c.set("app", this);
|
c.set("app", this);
|
||||||
await next();
|
await next();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// gracefully add the app id
|
||||||
|
c.res.headers.set("X-bknd-id", this._id);
|
||||||
|
} catch (e) {}
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
this.modules.ctx().emgr.registerEvents(AppEvents);
|
this.modules.ctx().emgr.registerEvents(AppEvents);
|
||||||
}
|
}
|
||||||
@@ -84,15 +107,18 @@ export class App {
|
|||||||
return this.modules.ctx().emgr;
|
return this.modules.ctx().emgr;
|
||||||
}
|
}
|
||||||
|
|
||||||
async build(options?: { sync?: boolean; drop?: boolean; save?: boolean }) {
|
async build(options?: { sync?: boolean; fetch?: boolean; forceBuild?: boolean }) {
|
||||||
await this.modules.build();
|
// prevent multiple concurrent builds
|
||||||
|
if (this._building) {
|
||||||
if (options?.sync) {
|
while (this._building) {
|
||||||
const syncResult = await this.module.data.em
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
.schema()
|
}
|
||||||
.sync({ force: true, drop: options.drop });
|
if (!options?.forceBuild) return;
|
||||||
//console.log("syncing", syncResult);
|
|
||||||
}
|
}
|
||||||
|
this._building = true;
|
||||||
|
|
||||||
|
if (options?.sync) this.modules.ctx().flags.sync_required = true;
|
||||||
|
await this.modules.build({ fetch: options?.fetch });
|
||||||
|
|
||||||
const { guard, server } = this.modules.ctx();
|
const { guard, server } = this.modules.ctx();
|
||||||
|
|
||||||
@@ -105,23 +131,24 @@ export class App {
|
|||||||
await Promise.all(this.plugins.map((plugin) => plugin(this)));
|
await Promise.all(this.plugins.map((plugin) => plugin(this)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$console.log("App built");
|
||||||
await this.emgr.emit(new AppBuiltEvent({ app: this }));
|
await this.emgr.emit(new AppBuiltEvent({ app: this }));
|
||||||
|
|
||||||
server.all("/api/*", async (c) => c.notFound());
|
|
||||||
|
|
||||||
if (options?.save) {
|
|
||||||
await this.modules.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
// first boot is set from ModuleManager when there wasn't a config table
|
// first boot is set from ModuleManager when there wasn't a config table
|
||||||
if (this.trigger_first_boot) {
|
if (this.trigger_first_boot) {
|
||||||
this.trigger_first_boot = false;
|
this.trigger_first_boot = false;
|
||||||
await this.emgr.emit(new AppFirstBoot({ app: this }));
|
await this.emgr.emit(new AppFirstBoot({ app: this }));
|
||||||
|
await this.options?.seed?.({
|
||||||
|
...this.modules.ctx(),
|
||||||
|
app: this,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._building = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateConfig<Module extends keyof Modules>(module: Module) {
|
mutateConfig<Module extends keyof Modules>(module: Module) {
|
||||||
return this.modules.get(module).schema();
|
return this.modules.mutateConfigSafe(module);
|
||||||
}
|
}
|
||||||
|
|
||||||
get server() {
|
get server() {
|
||||||
@@ -132,7 +159,7 @@ export class App {
|
|||||||
return this.modules.ctx().em;
|
return this.modules.ctx().em;
|
||||||
}
|
}
|
||||||
|
|
||||||
get fetch(): any {
|
get fetch(): Hono["fetch"] {
|
||||||
return this.server.fetch;
|
return this.server.fetch;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,8 +169,8 @@ export class App {
|
|||||||
{
|
{
|
||||||
get: (_, module: keyof Modules) => {
|
get: (_, module: keyof Modules) => {
|
||||||
return this.modules.get(module);
|
return this.modules.get(module);
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
) as Modules;
|
) as Modules;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,6 +182,10 @@ export class App {
|
|||||||
return this.modules.version();
|
return this.modules.version();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isBuilt(): boolean {
|
||||||
|
return this.modules.isBuilt();
|
||||||
|
}
|
||||||
|
|
||||||
registerAdminController(config?: AdminControllerOptions) {
|
registerAdminController(config?: AdminControllerOptions) {
|
||||||
// register admin
|
// register admin
|
||||||
this.adminController = new AdminController(this, config);
|
this.adminController = new AdminController(this, config);
|
||||||
@@ -173,6 +204,15 @@ export class App {
|
|||||||
async createUser(p: CreateUserPayload) {
|
async createUser(p: CreateUserPayload) {
|
||||||
return this.module.auth.createUser(p);
|
return this.module.auth.createUser(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getApi(options?: LocalApiOptions) {
|
||||||
|
const fetcher = this.server.request as typeof fetch;
|
||||||
|
if (options && options instanceof Request) {
|
||||||
|
return new Api({ request: options, headers: options.headers, fetcher });
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Api({ host: "http://localhost", ...(options ?? {}), fetcher });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createApp(config: CreateAppConfig = {}) {
|
export function createApp(config: CreateAppConfig = {}) {
|
||||||
@@ -182,18 +222,25 @@ export function createApp(config: CreateAppConfig = {}) {
|
|||||||
if (Connection.isConnection(config.connection)) {
|
if (Connection.isConnection(config.connection)) {
|
||||||
connection = config.connection;
|
connection = config.connection;
|
||||||
} else if (typeof config.connection === "object") {
|
} else if (typeof config.connection === "object") {
|
||||||
connection = new LibsqlConnection(config.connection.config);
|
if ("type" in config.connection) {
|
||||||
|
$console.warn(
|
||||||
|
"Using deprecated connection type 'libsql', use the 'config' object directly.",
|
||||||
|
);
|
||||||
|
connection = new LibsqlConnection(config.connection.config);
|
||||||
|
} else {
|
||||||
|
connection = new LibsqlConnection(config.connection);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
connection = new LibsqlConnection({ url: ":memory:" });
|
connection = new LibsqlConnection({ url: ":memory:" });
|
||||||
console.warn("[!] No connection provided, using in-memory database");
|
$console.warn("No connection provided, using in-memory database");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Could not create connection", e);
|
$console.error("Could not create connection", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!connection) {
|
if (!connection) {
|
||||||
throw new Error("Invalid connection");
|
throw new Error("Invalid connection");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new App(connection, config.initialConfig, config.plugins, config.options);
|
return new App(connection, config.initialConfig, config.options);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { type FrameworkBkndConfig, createFrameworkApp } from "adapter";
|
import type { App } from "bknd";
|
||||||
import { Api, type ApiOptions, type App } from "bknd";
|
import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter";
|
||||||
|
import { Api, type ApiOptions } from "bknd/client";
|
||||||
|
|
||||||
export type AstroBkndConfig = FrameworkBkndConfig;
|
export type AstroBkndConfig<Args = TAstro> = FrameworkBkndConfig<Args>;
|
||||||
|
|
||||||
type TAstro = {
|
type TAstro = {
|
||||||
request: Request;
|
request: Request;
|
||||||
@@ -13,18 +14,20 @@ export type Options = {
|
|||||||
host?: string;
|
host?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getApi(Astro: TAstro, options: Options = { mode: "static" }) {
|
export async function getApi(Astro: TAstro, options: Options = { mode: "static" }) {
|
||||||
return new Api({
|
const api = new Api({
|
||||||
host: new URL(Astro.request.url).origin,
|
host: new URL(Astro.request.url).origin,
|
||||||
headers: options.mode === "dynamic" ? Astro.request.headers : undefined
|
headers: options.mode === "dynamic" ? Astro.request.headers : undefined,
|
||||||
});
|
});
|
||||||
|
await api.verifyAuth();
|
||||||
|
return api;
|
||||||
}
|
}
|
||||||
|
|
||||||
let app: App;
|
let app: App;
|
||||||
export function serve(config: AstroBkndConfig = {}) {
|
export function serve<Context extends TAstro = TAstro>(config: AstroBkndConfig<Context> = {}) {
|
||||||
return async (args: TAstro) => {
|
return async (args: Context) => {
|
||||||
if (!app) {
|
if (!app) {
|
||||||
app = await createFrameworkApp(config);
|
app = await createFrameworkApp(config, args);
|
||||||
}
|
}
|
||||||
return app.fetch(args.request);
|
return app.fetch(args.request);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import type { App } from "bknd";
|
||||||
|
import { handle } from "hono/aws-lambda";
|
||||||
|
import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter";
|
||||||
|
|
||||||
|
export type AwsLambdaBkndConfig = RuntimeBkndConfig & {
|
||||||
|
assets?:
|
||||||
|
| {
|
||||||
|
mode: "local";
|
||||||
|
root: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
mode: "url";
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let app: App;
|
||||||
|
export async function createApp({
|
||||||
|
adminOptions = false,
|
||||||
|
assets,
|
||||||
|
...config
|
||||||
|
}: AwsLambdaBkndConfig = {}) {
|
||||||
|
if (!app) {
|
||||||
|
let additional: Partial<RuntimeBkndConfig> = {
|
||||||
|
adminOptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (assets?.mode) {
|
||||||
|
switch (assets.mode) {
|
||||||
|
case "local":
|
||||||
|
// @todo: serve static outside app context
|
||||||
|
additional = {
|
||||||
|
adminOptions: adminOptions === false ? undefined : adminOptions,
|
||||||
|
serveStatic: (await import("@hono/node-server/serve-static")).serveStatic({
|
||||||
|
root: assets.root,
|
||||||
|
onFound: (path, c) => {
|
||||||
|
c.res.headers.set("Cache-Control", "public, max-age=31536000");
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case "url":
|
||||||
|
additional.adminOptions = {
|
||||||
|
...(typeof adminOptions === "object" ? adminOptions : {}),
|
||||||
|
assets_path: assets.url,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Invalid assets mode");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app = await createRuntimeApp({
|
||||||
|
...config,
|
||||||
|
...additional,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serveLambda(config: AwsLambdaBkndConfig = {}) {
|
||||||
|
console.log("serving lambda");
|
||||||
|
return async (event) => {
|
||||||
|
const app = await createApp(config);
|
||||||
|
return await handle(app.server)(event);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./aws-lambda.adapter";
|
||||||
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import type { App } from "bknd";
|
import type { App } from "bknd";
|
||||||
|
import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter";
|
||||||
|
import { registerLocalMediaAdapter } from "bknd/adapter/node";
|
||||||
|
import { config } from "bknd/core";
|
||||||
import type { ServeOptions } from "bun";
|
import type { ServeOptions } from "bun";
|
||||||
import { config } from "core";
|
|
||||||
import { serveStatic } from "hono/bun";
|
import { serveStatic } from "hono/bun";
|
||||||
import { type RuntimeBkndConfig, createRuntimeApp } from "../index";
|
|
||||||
|
|
||||||
let app: App;
|
let app: App;
|
||||||
|
|
||||||
@@ -15,10 +16,10 @@ export async function createApp({ distPath, ...config }: RuntimeBkndConfig = {})
|
|||||||
const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static");
|
const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static");
|
||||||
|
|
||||||
if (!app) {
|
if (!app) {
|
||||||
|
registerLocalMediaAdapter();
|
||||||
app = await createRuntimeApp({
|
app = await createRuntimeApp({
|
||||||
...config,
|
...config,
|
||||||
registerLocalMedia: true,
|
serveStatic: serveStatic({ root }),
|
||||||
serveStatic: serveStatic({ root })
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,11 +30,11 @@ export function serve({
|
|||||||
distPath,
|
distPath,
|
||||||
connection,
|
connection,
|
||||||
initialConfig,
|
initialConfig,
|
||||||
plugins,
|
|
||||||
options,
|
options,
|
||||||
port = config.server.default_port,
|
port = config.server.default_port,
|
||||||
onBuilt,
|
onBuilt,
|
||||||
buildConfig,
|
buildConfig,
|
||||||
|
adminOptions,
|
||||||
...serveOptions
|
...serveOptions
|
||||||
}: BunBkndConfig = {}) {
|
}: BunBkndConfig = {}) {
|
||||||
Bun.serve({
|
Bun.serve({
|
||||||
@@ -43,14 +44,14 @@ export function serve({
|
|||||||
const app = await createApp({
|
const app = await createApp({
|
||||||
connection,
|
connection,
|
||||||
initialConfig,
|
initialConfig,
|
||||||
plugins,
|
|
||||||
options,
|
options,
|
||||||
onBuilt,
|
onBuilt,
|
||||||
buildConfig,
|
buildConfig,
|
||||||
distPath
|
adminOptions,
|
||||||
|
distPath,
|
||||||
});
|
});
|
||||||
return app.fetch(request);
|
return app.fetch(request);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Server is running on http://localhost:${port}`);
|
console.log(`Server is running on http://localhost:${port}`);
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/// <reference types="@cloudflare/workers-types" />
|
||||||
|
|
||||||
|
import { KyselyPluginRunner, SqliteConnection, SqliteIntrospector } from "bknd/data";
|
||||||
|
import type { QB } from "data/connection/Connection";
|
||||||
|
import { type DatabaseIntrospector, Kysely, ParseJSONResultsPlugin } from "kysely";
|
||||||
|
import { D1Dialect } from "kysely-d1";
|
||||||
|
|
||||||
|
export type D1ConnectionConfig = {
|
||||||
|
binding: D1Database;
|
||||||
|
};
|
||||||
|
|
||||||
|
class CustomD1Dialect extends D1Dialect {
|
||||||
|
override createIntrospector(db: Kysely<any>): DatabaseIntrospector {
|
||||||
|
return new SqliteIntrospector(db, {
|
||||||
|
excludeTables: ["_cf_KV"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class D1Connection extends SqliteConnection {
|
||||||
|
constructor(private config: D1ConnectionConfig) {
|
||||||
|
const plugins = [new ParseJSONResultsPlugin()];
|
||||||
|
|
||||||
|
const kysely = new Kysely({
|
||||||
|
dialect: new CustomD1Dialect({ database: config.binding }),
|
||||||
|
plugins,
|
||||||
|
});
|
||||||
|
super(kysely, {}, plugins);
|
||||||
|
}
|
||||||
|
|
||||||
|
override supportsBatching(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
override supportsIndices(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async batch<Queries extends QB[]>(
|
||||||
|
queries: [...Queries],
|
||||||
|
): Promise<{
|
||||||
|
[K in keyof Queries]: Awaited<ReturnType<Queries[K]["execute"]>>;
|
||||||
|
}> {
|
||||||
|
const db = this.config.binding;
|
||||||
|
|
||||||
|
const res = await db.batch(
|
||||||
|
queries.map((q) => {
|
||||||
|
const { sql, parameters } = q.compile();
|
||||||
|
return db.prepare(sql).bind(...parameters);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// let it run through plugins
|
||||||
|
const kyselyPlugins = new KyselyPluginRunner(this.plugins);
|
||||||
|
const data: any = [];
|
||||||
|
for (const r of res) {
|
||||||
|
const rows = await kyselyPlugins.transformResultRows(r.results);
|
||||||
|
data.push(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
+55
-14
@@ -1,6 +1,47 @@
|
|||||||
import { isDebug } from "core";
|
import { registries } from "bknd";
|
||||||
import type { FileBody, StorageAdapter } from "../Storage";
|
import { isDebug } from "bknd/core";
|
||||||
import { guessMimeType } from "../mime-types";
|
import { StringEnum, Type } from "bknd/utils";
|
||||||
|
import type { FileBody, StorageAdapter } from "media/storage/Storage";
|
||||||
|
import { guess } from "media/storage/mime-types-tiny";
|
||||||
|
import { getBindings } from "./bindings";
|
||||||
|
|
||||||
|
export function makeSchema(bindings: string[] = []) {
|
||||||
|
return Type.Object(
|
||||||
|
{
|
||||||
|
binding: bindings.length > 0 ? StringEnum(bindings) : Type.Optional(Type.String()),
|
||||||
|
},
|
||||||
|
{ title: "R2", description: "Cloudflare R2 storage" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerMedia(env: Record<string, any>) {
|
||||||
|
const r2_bindings = getBindings(env, "R2Bucket");
|
||||||
|
|
||||||
|
registries.media.register(
|
||||||
|
"r2",
|
||||||
|
class extends StorageR2Adapter {
|
||||||
|
constructor(private config: any) {
|
||||||
|
const binding = r2_bindings.find((b) => b.key === config.binding);
|
||||||
|
if (!binding) {
|
||||||
|
throw new Error(`No R2Bucket found with key ${config.binding}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
super(binding?.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
override getSchema() {
|
||||||
|
return makeSchema(r2_bindings.map((b) => b.key));
|
||||||
|
}
|
||||||
|
|
||||||
|
override toJSON() {
|
||||||
|
return {
|
||||||
|
...super.toJSON(),
|
||||||
|
config: this.config,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adapter for R2 storage
|
* Adapter for R2 storage
|
||||||
@@ -14,7 +55,7 @@ export class StorageR2Adapter implements StorageAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSchema() {
|
getSchema() {
|
||||||
return undefined;
|
return makeSchema();
|
||||||
}
|
}
|
||||||
|
|
||||||
async putObject(key: string, body: FileBody) {
|
async putObject(key: string, body: FileBody) {
|
||||||
@@ -26,13 +67,13 @@ export class StorageR2Adapter implements StorageAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async listObjects(
|
async listObjects(
|
||||||
prefix?: string
|
prefix?: string,
|
||||||
): Promise<{ key: string; last_modified: Date; size: number }[]> {
|
): Promise<{ key: string; last_modified: Date; size: number }[]> {
|
||||||
const list = await this.bucket.list({ limit: 50 });
|
const list = await this.bucket.list({ limit: 50 });
|
||||||
return list.objects.map((item) => ({
|
return list.objects.map((item) => ({
|
||||||
key: item.key,
|
key: item.key,
|
||||||
size: item.size,
|
size: item.size,
|
||||||
last_modified: item.uploaded
|
last_modified: item.uploaded,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +88,8 @@ export class StorageR2Adapter implements StorageAdapter {
|
|||||||
async getObject(key: string, headers: Headers): Promise<Response> {
|
async getObject(key: string, headers: Headers): Promise<Response> {
|
||||||
let object: R2ObjectBody | null;
|
let object: R2ObjectBody | null;
|
||||||
const responseHeaders = new Headers({
|
const responseHeaders = new Headers({
|
||||||
"Accept-Ranges": "bytes"
|
"Accept-Ranges": "bytes",
|
||||||
|
"Content-Type": guess(key),
|
||||||
});
|
});
|
||||||
|
|
||||||
//console.log("getObject:headers", headersToObject(headers));
|
//console.log("getObject:headers", headersToObject(headers));
|
||||||
@@ -56,7 +98,7 @@ export class StorageR2Adapter implements StorageAdapter {
|
|||||||
? {} // miniflare doesn't support range requests
|
? {} // miniflare doesn't support range requests
|
||||||
: {
|
: {
|
||||||
range: headers,
|
range: headers,
|
||||||
onlyIf: headers
|
onlyIf: headers,
|
||||||
};
|
};
|
||||||
object = (await this.bucket.get(key, options)) as R2ObjectBody;
|
object = (await this.bucket.get(key, options)) as R2ObjectBody;
|
||||||
|
|
||||||
@@ -88,7 +130,7 @@ export class StorageR2Adapter implements StorageAdapter {
|
|||||||
|
|
||||||
return new Response(object.body, {
|
return new Response(object.body, {
|
||||||
status: object.range ? 206 : 200,
|
status: object.range ? 206 : 200,
|
||||||
headers: responseHeaders
|
headers: responseHeaders,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,10 +139,9 @@ export class StorageR2Adapter implements StorageAdapter {
|
|||||||
if (!metadata || Object.keys(metadata).length === 0) {
|
if (!metadata || Object.keys(metadata).length === 0) {
|
||||||
// guessing is especially required for dev environment (miniflare)
|
// guessing is especially required for dev environment (miniflare)
|
||||||
metadata = {
|
metadata = {
|
||||||
contentType: guessMimeType(object.key)
|
contentType: guess(object.key),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
//console.log("writeHttpMetadata", object.httpMetadata, metadata);
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(metadata)) {
|
for (const [key, value] of Object.entries(metadata)) {
|
||||||
const camelToDash = key.replace(/([A-Z])/g, "-$1").toLowerCase();
|
const camelToDash = key.replace(/([A-Z])/g, "-$1").toLowerCase();
|
||||||
@@ -115,8 +156,8 @@ export class StorageR2Adapter implements StorageAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: String(head.httpMetadata?.contentType ?? "application/octet-stream"),
|
type: String(head.httpMetadata?.contentType ?? guess(key)),
|
||||||
size: head.size
|
size: head.size,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +172,7 @@ export class StorageR2Adapter implements StorageAdapter {
|
|||||||
toJSON(secrets?: boolean) {
|
toJSON(secrets?: boolean) {
|
||||||
return {
|
return {
|
||||||
type: this.getName(),
|
type: this.getName(),
|
||||||
config: {}
|
config: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
export type BindingTypeMap = {
|
||||||
|
D1Database: D1Database;
|
||||||
|
KVNamespace: KVNamespace;
|
||||||
|
DurableObjectNamespace: DurableObjectNamespace;
|
||||||
|
R2Bucket: R2Bucket;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetBindingType = keyof BindingTypeMap;
|
||||||
|
export type BindingMap<T extends GetBindingType> = { key: string; value: BindingTypeMap[T] };
|
||||||
|
|
||||||
|
export function getBindings<T extends GetBindingType>(env: any, type: T): BindingMap<T>[] {
|
||||||
|
const bindings: BindingMap<T>[] = [];
|
||||||
|
for (const key in env) {
|
||||||
|
try {
|
||||||
|
if (env[key] && (env[key] as any).constructor.name === type) {
|
||||||
|
bindings.push({
|
||||||
|
key,
|
||||||
|
value: env[key] as BindingTypeMap[T],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
return bindings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBinding<T extends GetBindingType>(env: any, type: T): BindingMap<T> {
|
||||||
|
const bindings = getBindings(env, type);
|
||||||
|
if (bindings.length === 0) {
|
||||||
|
throw new Error(`No ${type} found in bindings`);
|
||||||
|
}
|
||||||
|
return bindings[0] as BindingMap<T>;
|
||||||
|
}
|
||||||
@@ -1,18 +1,23 @@
|
|||||||
import type { CreateAppConfig } from "bknd";
|
/// <reference types="@cloudflare/workers-types" />
|
||||||
|
|
||||||
|
import { type FrameworkBkndConfig, makeConfig } from "bknd/adapter";
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { serveStatic } from "hono/cloudflare-workers";
|
import { serveStatic } from "hono/cloudflare-workers";
|
||||||
import type { FrameworkBkndConfig } from "../index";
|
import { D1Connection } from "./D1Connection";
|
||||||
|
import { registerMedia } from "./StorageR2Adapter";
|
||||||
|
import { getBinding } from "./bindings";
|
||||||
import { getCached } from "./modes/cached";
|
import { getCached } from "./modes/cached";
|
||||||
import { getDurable } from "./modes/durable";
|
import { getDurable } from "./modes/durable";
|
||||||
import { getFresh, getWarm } from "./modes/fresh";
|
import { getFresh, getWarm } from "./modes/fresh";
|
||||||
|
|
||||||
export type CloudflareBkndConfig<Env = any> = Omit<FrameworkBkndConfig, "app"> & {
|
export type CloudflareBkndConfig<Env = any> = FrameworkBkndConfig<Context<Env>> & {
|
||||||
app: CreateAppConfig | ((env: Env) => CreateAppConfig);
|
|
||||||
mode?: "warm" | "fresh" | "cache" | "durable";
|
mode?: "warm" | "fresh" | "cache" | "durable";
|
||||||
bindings?: (env: Env) => {
|
bindings?: (args: Context<Env>) => {
|
||||||
kv?: KVNamespace;
|
kv?: KVNamespace;
|
||||||
dobj?: DurableObjectNamespace;
|
dobj?: DurableObjectNamespace;
|
||||||
|
db?: D1Database;
|
||||||
};
|
};
|
||||||
|
static?: "kv" | "assets";
|
||||||
key?: string;
|
key?: string;
|
||||||
keepAliveSeconds?: number;
|
keepAliveSeconds?: number;
|
||||||
forceHttps?: boolean;
|
forceHttps?: boolean;
|
||||||
@@ -21,28 +26,65 @@ export type CloudflareBkndConfig<Env = any> = Omit<FrameworkBkndConfig, "app"> &
|
|||||||
html?: string;
|
html?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Context = {
|
export type Context<Env = any> = {
|
||||||
request: Request;
|
request: Request;
|
||||||
env: any;
|
env: Env;
|
||||||
ctx: ExecutionContext;
|
ctx: ExecutionContext;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function serve(config: CloudflareBkndConfig) {
|
let media_registered: boolean = false;
|
||||||
return {
|
export function makeCfConfig(config: CloudflareBkndConfig, context: Context) {
|
||||||
async fetch(request: Request, env: any, ctx: ExecutionContext) {
|
if (!media_registered) {
|
||||||
const url = new URL(request.url);
|
registerMedia(context.env as any);
|
||||||
const manifest = config.manifest;
|
media_registered = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (manifest) {
|
const appConfig = makeConfig(config, context);
|
||||||
|
const bindings = config.bindings?.(context);
|
||||||
|
if (!appConfig.connection) {
|
||||||
|
let db: D1Database | undefined;
|
||||||
|
if (bindings?.db) {
|
||||||
|
console.log("Using database from bindings");
|
||||||
|
db = bindings.db;
|
||||||
|
} else if (Object.keys(context.env ?? {}).length > 0) {
|
||||||
|
const binding = getBinding(context.env, "D1Database");
|
||||||
|
if (binding) {
|
||||||
|
console.log(`Using database from env "${binding.key}"`);
|
||||||
|
db = binding.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (db) {
|
||||||
|
appConfig.connection = new D1Connection({ binding: db });
|
||||||
|
} else {
|
||||||
|
throw new Error("No database connection given");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return appConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serve<Env = any>(config: CloudflareBkndConfig<Env> = {}) {
|
||||||
|
return {
|
||||||
|
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
if (config.manifest && config.static === "assets") {
|
||||||
|
console.warn("manifest is not useful with static 'assets'");
|
||||||
|
} else if (!config.manifest && config.static === "kv") {
|
||||||
|
throw new Error("manifest is required with static 'kv'");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.manifest && config.static !== "assets") {
|
||||||
const pathname = url.pathname.slice(1);
|
const pathname = url.pathname.slice(1);
|
||||||
const assetManifest = JSON.parse(manifest);
|
const assetManifest = JSON.parse(config.manifest);
|
||||||
if (pathname && pathname in assetManifest) {
|
if (pathname && pathname in assetManifest) {
|
||||||
const hono = new Hono();
|
const hono = new Hono();
|
||||||
|
|
||||||
hono.all("*", async (c, next) => {
|
hono.all("*", async (c, next) => {
|
||||||
const res = await serveStatic({
|
const res = await serveStatic({
|
||||||
path: `./${pathname}`,
|
path: `./${pathname}`,
|
||||||
manifest
|
manifest: config.manifest!,
|
||||||
})(c as any, next);
|
})(c as any, next);
|
||||||
if (res instanceof Response) {
|
if (res instanceof Response) {
|
||||||
const ttl = 60 * 60 * 24 * 365;
|
const ttl = 60 * 60 * 24 * 365;
|
||||||
@@ -57,8 +99,6 @@ export function serve(config: CloudflareBkndConfig) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config.setAdminHtml = config.setAdminHtml && !!config.manifest;
|
|
||||||
|
|
||||||
const context = { request, env, ctx } as Context;
|
const context = { request, env, ctx } as Context;
|
||||||
const mode = config.mode ?? "warm";
|
const mode = config.mode ?? "warm";
|
||||||
|
|
||||||
@@ -74,6 +114,6 @@ export function serve(config: CloudflareBkndConfig) {
|
|||||||
default:
|
default:
|
||||||
throw new Error(`Unknown mode ${mode}`);
|
throw new Error(`Unknown mode ${mode}`);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,18 @@
|
|||||||
|
import { D1Connection, type D1ConnectionConfig } from "./D1Connection";
|
||||||
|
|
||||||
export * from "./cloudflare-workers.adapter";
|
export * from "./cloudflare-workers.adapter";
|
||||||
export { makeApp, getFresh, getWarm } from "./modes/fresh";
|
export { makeApp, getFresh, getWarm } from "./modes/fresh";
|
||||||
export { getCached } from "./modes/cached";
|
export { getCached } from "./modes/cached";
|
||||||
export { DurableBkndApp, getDurable } from "./modes/durable";
|
export { DurableBkndApp, getDurable } from "./modes/durable";
|
||||||
|
export { D1Connection, type D1ConnectionConfig };
|
||||||
|
export {
|
||||||
|
getBinding,
|
||||||
|
getBindings,
|
||||||
|
type BindingTypeMap,
|
||||||
|
type GetBindingType,
|
||||||
|
type BindingMap,
|
||||||
|
} from "./bindings";
|
||||||
|
|
||||||
|
export function d1(config: D1ConnectionConfig) {
|
||||||
|
return new D1Connection(config);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { createRuntimeApp } from "adapter";
|
|
||||||
import { App } from "bknd";
|
import { App } from "bknd";
|
||||||
import type { CloudflareBkndConfig, Context } from "../index";
|
import { createRuntimeApp } from "bknd/adapter";
|
||||||
|
import { type CloudflareBkndConfig, type Context, makeCfConfig } from "../index";
|
||||||
|
|
||||||
export async function getCached(config: CloudflareBkndConfig, { env, ctx }: Context) {
|
export async function getCached(config: CloudflareBkndConfig, { env, ctx, ...args }: Context) {
|
||||||
const { kv } = config.bindings?.(env)!;
|
const { kv } = config.bindings?.(env)!;
|
||||||
if (!kv) throw new Error("kv namespace is not defined in cloudflare.bindings");
|
if (!kv) throw new Error("kv namespace is not defined in cloudflare.bindings");
|
||||||
const key = config.key ?? "app";
|
const key = config.key ?? "app";
|
||||||
@@ -16,7 +16,7 @@ export async function getCached(config: CloudflareBkndConfig, { env, ctx }: Cont
|
|||||||
|
|
||||||
const app = await createRuntimeApp(
|
const app = await createRuntimeApp(
|
||||||
{
|
{
|
||||||
...config,
|
...makeCfConfig(config, { env, ctx, ...args }),
|
||||||
initialConfig,
|
initialConfig,
|
||||||
onBuilt: async (app) => {
|
onBuilt: async (app) => {
|
||||||
app.module.server.client.get("/__bknd/cache", async (c) => {
|
app.module.server.client.get("/__bknd/cache", async (c) => {
|
||||||
@@ -31,13 +31,13 @@ export async function getCached(config: CloudflareBkndConfig, { env, ctx }: Cont
|
|||||||
async ({ params: { app } }) => {
|
async ({ params: { app } }) => {
|
||||||
saveConfig(app.toJSON(true));
|
saveConfig(app.toJSON(true));
|
||||||
},
|
},
|
||||||
"sync"
|
"sync",
|
||||||
);
|
);
|
||||||
await config.beforeBuild?.(app);
|
await config.beforeBuild?.(app);
|
||||||
},
|
},
|
||||||
adminOptions: { html: config.html }
|
adminOptions: { html: config.html },
|
||||||
},
|
},
|
||||||
env
|
{ env, ctx, ...args },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!cachedConfig) {
|
if (!cachedConfig) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { DurableObject } from "cloudflare:workers";
|
import { DurableObject } from "cloudflare:workers";
|
||||||
import { createRuntimeApp } from "adapter";
|
|
||||||
import type { CloudflareBkndConfig, Context } from "adapter/cloudflare";
|
|
||||||
import type { App, CreateAppConfig } from "bknd";
|
import type { App, CreateAppConfig } from "bknd";
|
||||||
|
import { createRuntimeApp, makeConfig } from "bknd/adapter";
|
||||||
|
import type { CloudflareBkndConfig, Context } from "../index";
|
||||||
|
|
||||||
export async function getDurable(config: CloudflareBkndConfig, ctx: Context) {
|
export async function getDurable(config: CloudflareBkndConfig, ctx: Context) {
|
||||||
const { dobj } = config.bindings?.(ctx.env)!;
|
const { dobj } = config.bindings?.(ctx.env)!;
|
||||||
@@ -17,13 +17,13 @@ export async function getDurable(config: CloudflareBkndConfig, ctx: Context) {
|
|||||||
const id = dobj.idFromName(key);
|
const id = dobj.idFromName(key);
|
||||||
const stub = dobj.get(id) as unknown as DurableBkndApp;
|
const stub = dobj.get(id) as unknown as DurableBkndApp;
|
||||||
|
|
||||||
const create_config = typeof config.app === "function" ? config.app(ctx.env) : config.app;
|
const create_config = makeConfig(config, ctx);
|
||||||
|
|
||||||
const res = await stub.fire(ctx.request, {
|
const res = await stub.fire(ctx.request, {
|
||||||
config: create_config,
|
config: create_config,
|
||||||
html: config.html,
|
html: config.html,
|
||||||
keepAliveSeconds: config.keepAliveSeconds,
|
keepAliveSeconds: config.keepAliveSeconds,
|
||||||
setAdminHtml: config.setAdminHtml
|
setAdminHtml: config.setAdminHtml,
|
||||||
});
|
});
|
||||||
|
|
||||||
const headers = new Headers(res.headers);
|
const headers = new Headers(res.headers);
|
||||||
@@ -32,7 +32,7 @@ export async function getDurable(config: CloudflareBkndConfig, ctx: Context) {
|
|||||||
return new Response(res.body, {
|
return new Response(res.body, {
|
||||||
status: res.status,
|
status: res.status,
|
||||||
statusText: res.statusText,
|
statusText: res.statusText,
|
||||||
headers
|
headers,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ export class DurableBkndApp extends DurableObject {
|
|||||||
html?: string;
|
html?: string;
|
||||||
keepAliveSeconds?: number;
|
keepAliveSeconds?: number;
|
||||||
setAdminHtml?: boolean;
|
setAdminHtml?: boolean;
|
||||||
}
|
},
|
||||||
) {
|
) {
|
||||||
let buildtime = 0;
|
let buildtime = 0;
|
||||||
if (!this.app) {
|
if (!this.app) {
|
||||||
@@ -73,7 +73,7 @@ export class DurableBkndApp extends DurableObject {
|
|||||||
return c.json({
|
return c.json({
|
||||||
id: this.id,
|
id: this.id,
|
||||||
keepAliveSeconds: options?.keepAliveSeconds ?? 0,
|
keepAliveSeconds: options?.keepAliveSeconds ?? 0,
|
||||||
colo: context.colo
|
colo: context.colo,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ export class DurableBkndApp extends DurableObject {
|
|||||||
adminOptions: { html: options.html },
|
adminOptions: { html: options.html },
|
||||||
beforeBuild: async (app) => {
|
beforeBuild: async (app) => {
|
||||||
await this.beforeBuild(app);
|
await this.beforeBuild(app);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
buildtime = performance.now() - start;
|
buildtime = performance.now() - start;
|
||||||
@@ -101,7 +101,7 @@ export class DurableBkndApp extends DurableObject {
|
|||||||
return new Response(res.body, {
|
return new Response(res.body, {
|
||||||
status: res.status,
|
status: res.status,
|
||||||
statusText: res.statusText,
|
statusText: res.statusText,
|
||||||
headers
|
headers,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { createRuntimeApp } from "adapter";
|
|
||||||
import type { App } from "bknd";
|
import type { App } from "bknd";
|
||||||
import type { CloudflareBkndConfig, Context } from "../index";
|
import { createRuntimeApp } from "bknd/adapter";
|
||||||
|
import { type CloudflareBkndConfig, type Context, makeCfConfig } from "../index";
|
||||||
|
|
||||||
export async function makeApp(config: CloudflareBkndConfig, { env }: Context) {
|
export async function makeApp(config: CloudflareBkndConfig, ctx: Context) {
|
||||||
return await createRuntimeApp(
|
return await createRuntimeApp(
|
||||||
{
|
{
|
||||||
...config,
|
...makeCfConfig(config, ctx),
|
||||||
adminOptions: config.html ? { html: config.html } : undefined
|
adminOptions: config.html ? { html: config.html } : undefined,
|
||||||
},
|
},
|
||||||
env
|
ctx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+19
-60
@@ -1,59 +1,31 @@
|
|||||||
import type { IncomingMessage } from "node:http";
|
import { App, type CreateAppConfig } from "bknd";
|
||||||
import { App, type CreateAppConfig, registries } from "bknd";
|
import { config as $config } from "bknd/core";
|
||||||
import { config as $config } from "core";
|
|
||||||
import type { MiddlewareHandler } from "hono";
|
import type { MiddlewareHandler } from "hono";
|
||||||
import { StorageLocalAdapter } from "media/storage/adapters/StorageLocalAdapter";
|
|
||||||
import type { AdminControllerOptions } from "modules/server/AdminController";
|
import type { AdminControllerOptions } from "modules/server/AdminController";
|
||||||
|
|
||||||
export type BkndConfig<Env = any> = CreateAppConfig & {
|
export type BkndConfig<Args = any> = CreateAppConfig & {
|
||||||
app?: CreateAppConfig | ((env: Env) => CreateAppConfig);
|
app?: CreateAppConfig | ((args: Args) => CreateAppConfig);
|
||||||
onBuilt?: (app: App) => Promise<void>;
|
onBuilt?: (app: App) => Promise<void>;
|
||||||
beforeBuild?: (app: App) => Promise<void>;
|
beforeBuild?: (app: App) => Promise<void>;
|
||||||
buildConfig?: Parameters<App["build"]>[0];
|
buildConfig?: Parameters<App["build"]>[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FrameworkBkndConfig<Env = any> = BkndConfig<Env>;
|
export type FrameworkBkndConfig<Args = any> = BkndConfig<Args>;
|
||||||
|
|
||||||
export type RuntimeBkndConfig<Env = any> = BkndConfig<Env> & {
|
export type RuntimeBkndConfig<Args = any> = BkndConfig<Args> & {
|
||||||
distPath?: string;
|
distPath?: string;
|
||||||
|
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
|
||||||
|
adminOptions?: AdminControllerOptions | false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function nodeRequestToRequest(req: IncomingMessage): Request {
|
export function makeConfig<Args = any>(config: BkndConfig<Args>, args?: Args): CreateAppConfig {
|
||||||
let protocol = "http";
|
|
||||||
try {
|
|
||||||
protocol = req.headers["x-forwarded-proto"] as string;
|
|
||||||
} catch (e) {}
|
|
||||||
const host = req.headers.host;
|
|
||||||
const url = `${protocol}://${host}${req.url}`;
|
|
||||||
const headers = new Headers();
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(req.headers)) {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
headers.append(key, value.join(", "));
|
|
||||||
} else if (value) {
|
|
||||||
headers.append(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const method = req.method || "GET";
|
|
||||||
return new Request(url, {
|
|
||||||
method,
|
|
||||||
headers
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function registerLocalMediaAdapter() {
|
|
||||||
registries.media.register("local", StorageLocalAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function makeConfig<Env = any>(config: BkndConfig<Env>, env?: Env): CreateAppConfig {
|
|
||||||
let additionalConfig: CreateAppConfig = {};
|
let additionalConfig: CreateAppConfig = {};
|
||||||
if ("app" in config && config.app) {
|
if ("app" in config && config.app) {
|
||||||
if (typeof config.app === "function") {
|
if (typeof config.app === "function") {
|
||||||
if (!env) {
|
if (!args) {
|
||||||
throw new Error("env is required when config.app is a function");
|
throw new Error("args is required when config.app is a function");
|
||||||
}
|
}
|
||||||
additionalConfig = config.app(env);
|
additionalConfig = config.app(args);
|
||||||
} else {
|
} else {
|
||||||
additionalConfig = config.app;
|
additionalConfig = config.app;
|
||||||
}
|
}
|
||||||
@@ -62,11 +34,11 @@ export function makeConfig<Env = any>(config: BkndConfig<Env>, env?: Env): Creat
|
|||||||
return { ...config, ...additionalConfig };
|
return { ...config, ...additionalConfig };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createFrameworkApp<Env = any>(
|
export async function createFrameworkApp<Args = any>(
|
||||||
config: FrameworkBkndConfig,
|
config: FrameworkBkndConfig,
|
||||||
env?: Env
|
args?: Args,
|
||||||
): Promise<App> {
|
): Promise<App> {
|
||||||
const app = App.create(makeConfig(config, env));
|
const app = App.create(makeConfig(config, args));
|
||||||
|
|
||||||
if (config.onBuilt) {
|
if (config.onBuilt) {
|
||||||
app.emgr.onEvent(
|
app.emgr.onEvent(
|
||||||
@@ -74,7 +46,7 @@ export async function createFrameworkApp<Env = any>(
|
|||||||
async () => {
|
async () => {
|
||||||
await config.onBuilt?.(app);
|
await config.onBuilt?.(app);
|
||||||
},
|
},
|
||||||
"sync"
|
"sync",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,22 +57,9 @@ export async function createFrameworkApp<Env = any>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createRuntimeApp<Env = any>(
|
export async function createRuntimeApp<Env = any>(
|
||||||
{
|
{ serveStatic, adminOptions, ...config }: RuntimeBkndConfig,
|
||||||
serveStatic,
|
env?: Env,
|
||||||
registerLocalMedia,
|
|
||||||
adminOptions,
|
|
||||||
...config
|
|
||||||
}: RuntimeBkndConfig & {
|
|
||||||
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
|
|
||||||
registerLocalMedia?: boolean;
|
|
||||||
adminOptions?: AdminControllerOptions | false;
|
|
||||||
},
|
|
||||||
env?: Env
|
|
||||||
): Promise<App> {
|
): Promise<App> {
|
||||||
if (registerLocalMedia) {
|
|
||||||
registerLocalMediaAdapter();
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = App.create(makeConfig(config, env));
|
const app = App.create(makeConfig(config, env));
|
||||||
|
|
||||||
app.emgr.onEvent(
|
app.emgr.onEvent(
|
||||||
@@ -118,7 +77,7 @@ export async function createRuntimeApp<Env = any>(
|
|||||||
app.registerAdminController(adminOptions);
|
app.registerAdminController(adminOptions);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sync"
|
"sync",
|
||||||
);
|
);
|
||||||
|
|
||||||
await config.beforeBuild?.(app);
|
await config.beforeBuild?.(app);
|
||||||
|
|||||||
@@ -1,60 +1,60 @@
|
|||||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
import type { App } from "bknd";
|
||||||
import { Api, type App } from "bknd";
|
import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter";
|
||||||
import { type FrameworkBkndConfig, createFrameworkApp, nodeRequestToRequest } from "../index";
|
import { getRuntimeKey, isNode } from "core/utils";
|
||||||
|
|
||||||
export type NextjsBkndConfig = FrameworkBkndConfig & {
|
export type NextjsBkndConfig = FrameworkBkndConfig & {
|
||||||
cleanSearch?: string[];
|
cleanRequest?: { searchParams?: string[] };
|
||||||
};
|
};
|
||||||
|
|
||||||
type GetServerSidePropsContext = {
|
let app: App;
|
||||||
req: IncomingMessage;
|
let building: boolean = false;
|
||||||
res: ServerResponse;
|
|
||||||
params?: Params;
|
|
||||||
query: any;
|
|
||||||
preview?: boolean;
|
|
||||||
previewData?: any;
|
|
||||||
draftMode?: boolean;
|
|
||||||
resolvedUrl: string;
|
|
||||||
locale?: string;
|
|
||||||
locales?: string[];
|
|
||||||
defaultLocale?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function createApi({ req }: GetServerSidePropsContext) {
|
export async function getApp(config: NextjsBkndConfig) {
|
||||||
const request = nodeRequestToRequest(req);
|
if (building) {
|
||||||
return new Api({
|
while (building) {
|
||||||
host: new URL(request.url).origin,
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||||
headers: request.headers
|
}
|
||||||
});
|
if (app) return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
building = true;
|
||||||
|
if (!app) {
|
||||||
|
app = await createFrameworkApp(config);
|
||||||
|
await app.build();
|
||||||
|
}
|
||||||
|
building = false;
|
||||||
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withApi<T>(handler: (ctx: GetServerSidePropsContext & { api: Api }) => T) {
|
function getCleanRequest(req: Request, cleanRequest: NextjsBkndConfig["cleanRequest"]) {
|
||||||
return (ctx: GetServerSidePropsContext & { api: Api }) => {
|
if (!cleanRequest) return req;
|
||||||
return handler({ ...ctx, api: createApi(ctx) });
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCleanRequest(
|
|
||||||
req: Request,
|
|
||||||
{ cleanSearch = ["route"] }: Pick<NextjsBkndConfig, "cleanSearch">
|
|
||||||
) {
|
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
cleanSearch?.forEach((k) => url.searchParams.delete(k));
|
cleanRequest?.searchParams?.forEach((k) => url.searchParams.delete(k));
|
||||||
|
|
||||||
|
if (isNode()) {
|
||||||
|
return new Request(url.toString(), {
|
||||||
|
method: req.method,
|
||||||
|
headers: req.headers,
|
||||||
|
body: req.body,
|
||||||
|
// @ts-ignore
|
||||||
|
duplex: "half",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return new Request(url.toString(), {
|
return new Request(url.toString(), {
|
||||||
method: req.method,
|
method: req.method,
|
||||||
headers: req.headers,
|
headers: req.headers,
|
||||||
body: req.body
|
body: req.body,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let app: App;
|
export function serve({ cleanRequest, ...config }: NextjsBkndConfig = {}) {
|
||||||
export function serve({ cleanSearch, ...config }: NextjsBkndConfig = {}) {
|
|
||||||
return async (req: Request) => {
|
return async (req: Request) => {
|
||||||
if (!app) {
|
if (!app) {
|
||||||
app = await createFrameworkApp(config);
|
app = await getApp(config);
|
||||||
}
|
}
|
||||||
const request = getCleanRequest(req, { cleanSearch });
|
const request = getCleanRequest(req, cleanRequest);
|
||||||
return app.fetch(request, process.env);
|
return app.fetch(request);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
export * from "./node.adapter";
|
import { registries } from "bknd";
|
||||||
export {
|
import {
|
||||||
|
type LocalAdapterConfig,
|
||||||
StorageLocalAdapter,
|
StorageLocalAdapter,
|
||||||
type LocalAdapterConfig
|
|
||||||
} from "../../media/storage/adapters/StorageLocalAdapter";
|
} from "../../media/storage/adapters/StorageLocalAdapter";
|
||||||
export { registerLocalMediaAdapter } from "../index";
|
|
||||||
|
export * from "./node.adapter";
|
||||||
|
export { StorageLocalAdapter, type LocalAdapterConfig };
|
||||||
|
|
||||||
|
export function registerLocalMediaAdapter() {
|
||||||
|
registries.media.register("local", StorageLocalAdapter);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { serve as honoServe } from "@hono/node-server";
|
import { serve as honoServe } from "@hono/node-server";
|
||||||
import { serveStatic } from "@hono/node-server/serve-static";
|
import { serveStatic } from "@hono/node-server/serve-static";
|
||||||
|
import { registerLocalMediaAdapter } from "adapter/node/index";
|
||||||
import type { App } from "bknd";
|
import type { App } from "bknd";
|
||||||
import { config as $config } from "core";
|
import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter";
|
||||||
import { type RuntimeBkndConfig, createRuntimeApp } from "../index";
|
import { config as $config } from "bknd/core";
|
||||||
|
|
||||||
export type NodeBkndConfig = RuntimeBkndConfig & {
|
export type NodeBkndConfig = RuntimeBkndConfig & {
|
||||||
port?: number;
|
port?: number;
|
||||||
@@ -23,7 +24,7 @@ export function serve({
|
|||||||
}: NodeBkndConfig = {}) {
|
}: NodeBkndConfig = {}) {
|
||||||
const root = path.relative(
|
const root = path.relative(
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
path.resolve(distPath ?? relativeDistPath ?? "./node_modules/bknd/dist", "static")
|
path.resolve(distPath ?? relativeDistPath ?? "./node_modules/bknd/dist", "static"),
|
||||||
);
|
);
|
||||||
if (relativeDistPath) {
|
if (relativeDistPath) {
|
||||||
console.warn("relativeDistPath is deprecated, please use distPath instead");
|
console.warn("relativeDistPath is deprecated, please use distPath instead");
|
||||||
@@ -37,19 +38,19 @@ export function serve({
|
|||||||
hostname,
|
hostname,
|
||||||
fetch: async (req: Request) => {
|
fetch: async (req: Request) => {
|
||||||
if (!app) {
|
if (!app) {
|
||||||
|
registerLocalMediaAdapter();
|
||||||
app = await createRuntimeApp({
|
app = await createRuntimeApp({
|
||||||
...config,
|
...config,
|
||||||
registerLocalMedia: true,
|
serveStatic: serveStatic({ root }),
|
||||||
serveStatic: serveStatic({ root })
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return app.fetch(req);
|
return app.fetch(req);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
(connInfo) => {
|
(connInfo) => {
|
||||||
console.log(`Server is running on http://localhost:${connInfo.port}`);
|
console.log(`Server is running on http://localhost:${connInfo.port}`);
|
||||||
listener?.(connInfo);
|
listener?.(connInfo);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import { useAuth } from "bknd/client";
|
||||||
import type { BkndAdminProps } from "bknd/ui";
|
import type { BkndAdminProps } from "bknd/ui";
|
||||||
import { Suspense, lazy, useEffect, useState } from "react";
|
import { Suspense, lazy, useEffect, useState } from "react";
|
||||||
|
|
||||||
export function adminPage(props?: BkndAdminProps) {
|
export function adminPage(props?: BkndAdminProps) {
|
||||||
const Admin = lazy(() => import("bknd/ui").then((mod) => ({ default: mod.Admin })));
|
const Admin = lazy(() => import("bknd/ui").then((mod) => ({ default: mod.Admin })));
|
||||||
return () => {
|
return () => {
|
||||||
|
const auth = useAuth();
|
||||||
const [loaded, setLoaded] = useState(false);
|
const [loaded, setLoaded] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
@@ -13,7 +15,7 @@ export function adminPage(props?: BkndAdminProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<Admin {...props} />
|
<Admin withProvider={{ user: auth.user }} {...props} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +1,40 @@
|
|||||||
import { type FrameworkBkndConfig, createFrameworkApp } from "adapter";
|
|
||||||
import type { App } from "bknd";
|
import type { App } from "bknd";
|
||||||
|
import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter";
|
||||||
|
|
||||||
export type RemixBkndConfig = FrameworkBkndConfig;
|
export type RemixBkndConfig<Args = RemixContext> = FrameworkBkndConfig<Args>;
|
||||||
|
|
||||||
|
type RemixContext = {
|
||||||
|
request: Request;
|
||||||
|
};
|
||||||
|
|
||||||
let app: App;
|
let app: App;
|
||||||
export function serve(config: RemixBkndConfig = {}) {
|
let building: boolean = false;
|
||||||
return async (args: { request: Request }) => {
|
|
||||||
if (!app) {
|
export async function getApp<Args extends RemixContext = RemixContext>(
|
||||||
app = await createFrameworkApp(config);
|
config: RemixBkndConfig<Args>,
|
||||||
|
args?: Args
|
||||||
|
) {
|
||||||
|
if (building) {
|
||||||
|
while (building) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||||
}
|
}
|
||||||
|
if (app) return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
building = true;
|
||||||
|
if (!app) {
|
||||||
|
app = await createFrameworkApp(config, args);
|
||||||
|
await app.build();
|
||||||
|
}
|
||||||
|
building = false;
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serve<Args extends RemixContext = RemixContext>(
|
||||||
|
config: RemixBkndConfig<Args> = {},
|
||||||
|
) {
|
||||||
|
return async (args: Args) => {
|
||||||
|
app = await getApp(config, args);
|
||||||
return app.fetch(args.request);
|
return app.fetch(args.request);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import type { IncomingMessage } from "node:http";
|
||||||
|
|
||||||
|
export function nodeRequestToRequest(req: IncomingMessage): Request {
|
||||||
|
let protocol = "http";
|
||||||
|
try {
|
||||||
|
protocol = req.headers["x-forwarded-proto"] as string;
|
||||||
|
} catch (e) {}
|
||||||
|
const host = req.headers.host;
|
||||||
|
const url = `${protocol}://${host}${req.url}`;
|
||||||
|
const headers = new Headers();
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(req.headers)) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
headers.append(key, value.join(", "));
|
||||||
|
} else if (value) {
|
||||||
|
headers.append(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const method = req.method || "GET";
|
||||||
|
return new Request(url, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user