mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
Merge remote-tracking branch 'origin/release/0.10' into feat/remove-admin-config
# Conflicts: # app/src/modules/server/AdminController.tsx # app/src/ui/Admin.tsx
This commit is contained in:
26
.github/workflows/test.yml
vendored
Normal file
26
.github/workflows/test.yml
vendored
Normal file
@@ -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
|
||||
14
README.md
14
README.md
@@ -4,7 +4,7 @@
|
||||

|
||||
|
||||
<p align="center" width="100%">
|
||||
<a href="https://stackblitz.com/github/bknd-io/bknd-examples?hideExplorer=1&embed=1&view=preview&startScript=example-admin-rich&initialPath=%2Fdata%2Fschema">
|
||||
<a href="https://stackblitz.com/github/bknd-io/bknd-examples?hideExplorer=1&embed=1&view=preview&startScript=example-admin-rich&initialPath=%2Fdata%2Fschema" target="_blank">
|
||||
<strong>⭐ Live Demo</strong>
|
||||
</a>
|
||||
</p>
|
||||
@@ -18,13 +18,15 @@ bknd simplifies app development by providing a fully functional backend for data
|
||||
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
|
||||
|
||||
## Size
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
The size on npm is misleading, as the `bknd` package includes the backend, the ui components as well as the whole backend bundled into the cli including static assets.
|
||||
|
||||
Depending on what you use, the size can be higher as additional dependencies are getting pulled in. The minimal size of a full `bknd` app as an API is around 212 kB gzipped (e.g. deployed as Cloudflare Worker).
|
||||
|
||||
## Motivation
|
||||
Creating digital products always requires developing both the backend (the logic) and the frontend (the appearance). Building a backend from scratch demands deep knowledge in areas such as authentication and database management. Using a backend framework can speed up initial development, but it still requires ongoing effort to work within its constraints (e.g., *"how to do X with Y?"*), which can quickly slow you down. Choosing a backend system is a tough decision, as you might not be aware of its limitations until you encounter them.
|
||||
|
||||
@@ -78,7 +80,7 @@ export default function AdminPage() {
|
||||
### Using the REST API or TypeScript SDK (`bknd/client`)
|
||||
If you're not using a JavaScript environment, you can still access any endpoint using the REST API:
|
||||
```bash
|
||||
curl -XGET <your-endpoint>/api/data/<entity>
|
||||
curl -XGET <your-endpoint>/api/data/entity/<entity>
|
||||
{
|
||||
"data": [
|
||||
{ "id": 1, ... },
|
||||
|
||||
@@ -19,8 +19,8 @@ describe("Api", async () => {
|
||||
const token = await sign({ foo: "bar" }, "1234");
|
||||
const request = new Request("http://example.com/test", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
const api = new Api({ request });
|
||||
expect(api.isAuthVerified()).toBe(false);
|
||||
@@ -35,8 +35,8 @@ describe("Api", async () => {
|
||||
const token = await sign({ foo: "bar" }, "1234");
|
||||
const request = new Request("http://example.com/test", {
|
||||
headers: {
|
||||
Cookie: `auth=${token}`
|
||||
}
|
||||
Cookie: `auth=${token}`,
|
||||
},
|
||||
});
|
||||
const api = new Api({ request });
|
||||
expect(api.isAuthVerified()).toBe(false);
|
||||
|
||||
@@ -26,7 +26,7 @@ describe("DataApi", () => {
|
||||
|
||||
it("returns result", async () => {
|
||||
const schema = proto.em({
|
||||
posts: proto.entity("posts", { title: proto.text() })
|
||||
posts: proto.entity("posts", { title: proto.text() }),
|
||||
});
|
||||
const em = schemaToEm(schema);
|
||||
await em.schema().sync({ force: true });
|
||||
@@ -40,19 +40,17 @@ describe("DataApi", () => {
|
||||
|
||||
{
|
||||
const res = (await app.request("/entity/posts")) as Response;
|
||||
const { data } = await res.json();
|
||||
const { data } = (await res.json()) as any;
|
||||
expect(data.length).toEqual(3);
|
||||
}
|
||||
|
||||
// @ts-ignore tests
|
||||
const api = new DataApi({ basepath: "/", queryLengthLimit: 50 });
|
||||
// @ts-ignore protected
|
||||
api.fetcher = app.request as typeof fetch;
|
||||
const api = new DataApi({ basepath: "/", queryLengthLimit: 50 }, app.request as typeof fetch);
|
||||
{
|
||||
const req = api.readMany("posts", { select: ["title"] });
|
||||
expect(req.request.method).toBe("GET");
|
||||
const res = await req;
|
||||
expect(res.data).toEqual(payload);
|
||||
expect(res.data).toEqual(payload as any);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -60,11 +58,155 @@ describe("DataApi", () => {
|
||||
select: ["title"],
|
||||
limit: 100000,
|
||||
offset: 0,
|
||||
sort: "id"
|
||||
sort: "id",
|
||||
});
|
||||
expect(req.request.method).toBe("POST");
|
||||
const res = await req;
|
||||
expect(res.data).toEqual(payload);
|
||||
expect(res.data).toEqual(payload as any);
|
||||
}
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,8 +18,8 @@ const mockedBackend = new Hono()
|
||||
return new Response(file, {
|
||||
headers: {
|
||||
"Content-Type": file.type,
|
||||
"Content-Length": file.size.toString()
|
||||
}
|
||||
"Content-Length": file.size.toString(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,15 +30,15 @@ describe("MediaApi", () => {
|
||||
// @ts-ignore tests
|
||||
const api = new MediaApi({
|
||||
host,
|
||||
basepath
|
||||
basepath,
|
||||
});
|
||||
expect(api.getFileUploadUrl({ path: "path" })).toBe(`${host}${basepath}/upload/path`);
|
||||
expect(api.getFileUploadUrl({ path: "path" } as any)).toBe(`${host}${basepath}/upload/path`);
|
||||
});
|
||||
|
||||
it("should have correct upload headers", () => {
|
||||
// @ts-ignore tests
|
||||
const api = new MediaApi({
|
||||
token: "token"
|
||||
token: "token",
|
||||
});
|
||||
expect(api.getUploadHeaders().get("Authorization")).toBe("Bearer token");
|
||||
});
|
||||
@@ -139,7 +139,7 @@ describe("MediaApi", () => {
|
||||
const response = (await mockedBackend.request(url)) as Response;
|
||||
await matches(
|
||||
await api.upload(response.body!, { filename: "readable.png" }),
|
||||
"readable.png"
|
||||
"readable.png",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -61,7 +61,7 @@ describe("ModuleApi", () => {
|
||||
|
||||
it("adds additional headers from options", () => {
|
||||
const headers = new Headers({
|
||||
"X-Test": "123"
|
||||
"X-Test": "123",
|
||||
});
|
||||
const api = new Api({ host, headers });
|
||||
expect(api.get("/").request.headers.get("X-Test")).toEqual("123");
|
||||
@@ -75,7 +75,7 @@ describe("ModuleApi", () => {
|
||||
it("uses search params", () => {
|
||||
const api = new Api({ host });
|
||||
const search = new URLSearchParams({
|
||||
foo: "bar"
|
||||
foo: "bar",
|
||||
});
|
||||
expect(api.get("/", search).request.url).toEqual("http://localhost/?" + search.toString());
|
||||
});
|
||||
@@ -89,6 +89,14 @@ describe("ModuleApi", () => {
|
||||
expect(api.delete("/").request.method).toEqual("DELETE");
|
||||
});
|
||||
|
||||
it("refines", async () => {
|
||||
const app = new Hono().get("/endpoint", (c) => c.json({ foo: ["bar"] }));
|
||||
const api = new Api({ host }, app.request as typeof fetch);
|
||||
|
||||
expect((await api.get("/endpoint")).data).toEqual({ foo: ["bar"] });
|
||||
expect((await api.get("/endpoint").refine((data) => data.foo)).data).toEqual(["bar"]);
|
||||
});
|
||||
|
||||
// @todo: test error response
|
||||
// @todo: test method shortcut functions
|
||||
});
|
||||
|
||||
54
app/__test__/app/App.spec.ts
Normal file
54
app/__test__/app/App.spec.ts
Normal file
@@ -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: {
|
||||
type: "local",
|
||||
config: {
|
||||
path: "./"
|
||||
}
|
||||
}
|
||||
path: "./",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(config.enabled).toBe(true);
|
||||
@@ -38,9 +38,9 @@ describe("repros", async () => {
|
||||
"entities.test",
|
||||
proto
|
||||
.entity("test", {
|
||||
content: proto.text()
|
||||
content: proto.text(),
|
||||
})
|
||||
.toJSON()
|
||||
.toJSON(),
|
||||
);
|
||||
expect(app.em.entities.map((e) => e.name)).toContain("test");
|
||||
}
|
||||
@@ -54,8 +54,8 @@ describe("repros", async () => {
|
||||
hidden: false,
|
||||
mime_types: [],
|
||||
virtual: true,
|
||||
entity: "test"
|
||||
}
|
||||
entity: "test",
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
@@ -63,8 +63,8 @@ describe("repros", async () => {
|
||||
type: "poly",
|
||||
source: "test",
|
||||
target: "media",
|
||||
config: { mappedBy: "files" }
|
||||
})
|
||||
config: { mappedBy: "files" },
|
||||
}),
|
||||
).resolves.toBeDefined();
|
||||
}
|
||||
|
||||
@@ -75,17 +75,17 @@ describe("repros", async () => {
|
||||
const schema = proto.em(
|
||||
{
|
||||
products: proto.entity("products", {
|
||||
title: proto.text()
|
||||
title: proto.text(),
|
||||
}),
|
||||
product_likes: proto.entity("product_likes", {
|
||||
created_at: proto.date()
|
||||
created_at: proto.date(),
|
||||
}),
|
||||
users: proto.entity("users", {})
|
||||
users: proto.entity("users", {}),
|
||||
},
|
||||
(fns, schema) => {
|
||||
fns.relation(schema.product_likes).manyToOne(schema.products, { inversedBy: "likes" });
|
||||
fns.relation(schema.product_likes).manyToOne(schema.users);
|
||||
}
|
||||
},
|
||||
);
|
||||
const app = createApp({ initialConfig: { data: schema.toJSON() } });
|
||||
await app.build();
|
||||
@@ -96,8 +96,8 @@ describe("repros", async () => {
|
||||
expect(info.relations.listable).toEqual([
|
||||
{
|
||||
entity: "product_likes",
|
||||
ref: "likes"
|
||||
}
|
||||
ref: "likes",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,13 +7,13 @@ describe("authorize", () => {
|
||||
["read", "write"],
|
||||
{
|
||||
admin: {
|
||||
permissions: ["read", "write"]
|
||||
}
|
||||
permissions: ["read", "write"],
|
||||
},
|
||||
{ enabled: true }
|
||||
},
|
||||
{ enabled: true },
|
||||
);
|
||||
const user = {
|
||||
role: "admin"
|
||||
role: "admin",
|
||||
};
|
||||
|
||||
expect(guard.granted("read", user)).toBe(true);
|
||||
@@ -27,21 +27,21 @@ describe("authorize", () => {
|
||||
["read", "write"],
|
||||
{
|
||||
admin: {
|
||||
permissions: ["read", "write"]
|
||||
permissions: ["read", "write"],
|
||||
},
|
||||
guest: {
|
||||
permissions: ["read"],
|
||||
is_default: true
|
||||
}
|
||||
is_default: true,
|
||||
},
|
||||
{ enabled: true }
|
||||
},
|
||||
{ enabled: true },
|
||||
);
|
||||
|
||||
expect(guard.granted("read")).toBe(true);
|
||||
expect(guard.granted("write")).toBe(false);
|
||||
|
||||
const user = {
|
||||
role: "admin"
|
||||
role: "admin",
|
||||
};
|
||||
|
||||
expect(guard.granted("read", user)).toBe(true);
|
||||
@@ -58,12 +58,12 @@ describe("authorize", () => {
|
||||
test("role implicit allow", async () => {
|
||||
const guard = Guard.create(["read", "write"], {
|
||||
admin: {
|
||||
implicit_allow: true
|
||||
}
|
||||
implicit_allow: true,
|
||||
},
|
||||
});
|
||||
|
||||
const user = {
|
||||
role: "admin"
|
||||
role: "admin",
|
||||
};
|
||||
|
||||
expect(guard.granted("read", user)).toBe(true);
|
||||
@@ -74,8 +74,8 @@ describe("authorize", () => {
|
||||
const guard = Guard.create(["read", "write"], {
|
||||
guest: {
|
||||
implicit_allow: true,
|
||||
is_default: true
|
||||
}
|
||||
is_default: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(guard.getUserRole()?.name).toBe("guest");
|
||||
|
||||
@@ -7,10 +7,10 @@ describe("OAuthStrategy", async () => {
|
||||
const strategy = new OAuthStrategy({
|
||||
type: "oidc",
|
||||
client: {
|
||||
client_id: process.env.OAUTH_CLIENT_ID,
|
||||
client_secret: process.env.OAUTH_CLIENT_SECRET
|
||||
client_id: process.env.OAUTH_CLIENT_ID!,
|
||||
client_secret: process.env.OAUTH_CLIENT_SECRET!,
|
||||
},
|
||||
name: "google"
|
||||
name: "google",
|
||||
});
|
||||
const state = "---";
|
||||
const redirect_uri = "http://localhost:3000/auth/google/callback";
|
||||
@@ -19,11 +19,6 @@ describe("OAuthStrategy", async () => {
|
||||
const config = await strategy.getConfig();
|
||||
console.log("config", JSON.stringify(config, null, 2));
|
||||
|
||||
const request = await strategy.request({
|
||||
redirect_uri,
|
||||
state
|
||||
});
|
||||
|
||||
const server = Bun.serve({
|
||||
fetch: async (req) => {
|
||||
const url = new URL(req.url);
|
||||
@@ -31,13 +26,18 @@ describe("OAuthStrategy", async () => {
|
||||
console.log("req", req);
|
||||
const user = await strategy.callback(url, {
|
||||
redirect_uri,
|
||||
state
|
||||
state,
|
||||
});
|
||||
|
||||
console.log("---user", user);
|
||||
}
|
||||
return new Response("Bun!");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const request = await strategy.request({
|
||||
redirect_uri,
|
||||
state,
|
||||
});
|
||||
console.log("request", request);
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ class ReturnEvent extends Event<{ foo: string }, string> {
|
||||
}
|
||||
|
||||
return this.clone({
|
||||
foo: [this.params.foo, value].join("-")
|
||||
foo: [this.params.foo, value].join("-"),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ describe("EventManager", async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
delayed();
|
||||
},
|
||||
"sync"
|
||||
"sync",
|
||||
);
|
||||
|
||||
// don't allow unknown
|
||||
@@ -83,8 +83,8 @@ describe("EventManager", async () => {
|
||||
const emgr = new EventManager(
|
||||
{ InformationalEvent },
|
||||
{
|
||||
asyncExecutor
|
||||
}
|
||||
asyncExecutor,
|
||||
},
|
||||
);
|
||||
|
||||
emgr.onEvent(InformationalEvent, async () => {});
|
||||
@@ -98,8 +98,8 @@ describe("EventManager", async () => {
|
||||
const emgr = new EventManager(
|
||||
{ ReturnEvent, InformationalEvent },
|
||||
{
|
||||
onInvalidReturn
|
||||
}
|
||||
onInvalidReturn,
|
||||
},
|
||||
);
|
||||
|
||||
// @ts-expect-error InformationalEvent has no return value
|
||||
@@ -140,7 +140,7 @@ describe("EventManager", async () => {
|
||||
expect(slug).toBe("informational-event");
|
||||
call();
|
||||
},
|
||||
{ mode: "sync", once: true }
|
||||
{ mode: "sync", once: true },
|
||||
);
|
||||
|
||||
expect(emgr.getListeners().length).toBe(1);
|
||||
|
||||
@@ -30,8 +30,8 @@ describe("Registry", () => {
|
||||
first: {
|
||||
cls: What,
|
||||
schema: Type.Object({ type: Type.String(), what: Type.String() }),
|
||||
enabled: true
|
||||
}
|
||||
enabled: true,
|
||||
},
|
||||
} satisfies Record<string, Test1>);
|
||||
|
||||
const item = registry.get("first");
|
||||
@@ -42,7 +42,7 @@ describe("Registry", () => {
|
||||
registry.add("second", {
|
||||
cls: What2,
|
||||
schema: second,
|
||||
enabled: true
|
||||
enabled: true,
|
||||
});
|
||||
// @ts-ignore
|
||||
expect(registry.get("second").schema).toEqual(second);
|
||||
@@ -52,7 +52,7 @@ describe("Registry", () => {
|
||||
// @ts-expect-error
|
||||
cls: NotAllowed,
|
||||
schema: third,
|
||||
enabled: true
|
||||
enabled: true,
|
||||
});
|
||||
// @ts-ignore
|
||||
expect(registry.get("third").schema).toEqual(third);
|
||||
@@ -62,7 +62,7 @@ describe("Registry", () => {
|
||||
cls: What,
|
||||
// @ts-expect-error
|
||||
schema: fourth,
|
||||
enabled: true
|
||||
enabled: true,
|
||||
});
|
||||
// @ts-ignore
|
||||
expect(registry.get("fourth").schema).toEqual(fourth);
|
||||
@@ -75,7 +75,7 @@ describe("Registry", () => {
|
||||
return {
|
||||
cls: a,
|
||||
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 {
|
||||
CloudflareKVCacheItem,
|
||||
CloudflareKVCachePool
|
||||
CloudflareKVCachePool,
|
||||
} from "../../../src/core/cache/adapters/CloudflareKvCache";
|
||||
import { runTests } from "./cache-test-suite";
|
||||
|
||||
@@ -26,7 +26,7 @@ describe("CloudflareKv", async () => {
|
||||
mf = new Miniflare({
|
||||
modules: true,
|
||||
script: "export default { async fetch() { return new Response(null); } }",
|
||||
kvNamespaces: ["TEST"]
|
||||
kvNamespaces: ["TEST"],
|
||||
});
|
||||
const kv = await mf.getKVNamespace("TEST");
|
||||
return new CloudflareKVCachePool(kv as any);
|
||||
@@ -45,10 +45,10 @@ describe("CloudflareKv", async () => {
|
||||
},
|
||||
toBeUndefined() {
|
||||
assert.equal(actual, undefined);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
|
||||
4
app/__test__/core/cache/MemoryCache.spec.ts
vendored
4
app/__test__/core/cache/MemoryCache.spec.ts
vendored
@@ -9,7 +9,7 @@ describe("MemoryCache", () => {
|
||||
tester: {
|
||||
test,
|
||||
beforeEach,
|
||||
expect
|
||||
}
|
||||
expect,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { checksum, hash } from "../../src/core/utils";
|
||||
describe("crypto", async () => {
|
||||
test("sha256", async () => {
|
||||
expect(await hash.sha256("test")).toBe(
|
||||
"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
|
||||
"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
|
||||
);
|
||||
});
|
||||
test("sha1", async () => {
|
||||
|
||||
35
app/__test__/core/env.spec.ts
Normal file
35
app/__test__/core/env.spec.ts
Normal file
@@ -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" }) }),
|
||||
{ a: "test" },
|
||||
{
|
||||
forceParse: true
|
||||
}
|
||||
forceParse: true,
|
||||
},
|
||||
);
|
||||
|
||||
expect(m.get()).toEqual({ a: "test" });
|
||||
@@ -30,14 +30,14 @@ describe("SchemaObject", async () => {
|
||||
b: Type.Object(
|
||||
{
|
||||
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" } } });
|
||||
|
||||
@@ -59,8 +59,8 @@ describe("SchemaObject", async () => {
|
||||
test("patch array", async () => {
|
||||
const m = new SchemaObject(
|
||||
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"] });
|
||||
|
||||
@@ -81,14 +81,14 @@ describe("SchemaObject", async () => {
|
||||
a: Type.String({ default: "b" }),
|
||||
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" } } });
|
||||
|
||||
@@ -108,8 +108,8 @@ describe("SchemaObject", async () => {
|
||||
test("set", async () => {
|
||||
const m = new SchemaObject(
|
||||
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"] });
|
||||
|
||||
@@ -125,7 +125,7 @@ describe("SchemaObject", async () => {
|
||||
let result: any;
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] })
|
||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
|
||||
}),
|
||||
undefined,
|
||||
{
|
||||
@@ -133,8 +133,8 @@ describe("SchemaObject", async () => {
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
called = true;
|
||||
result = config;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await m.set({ methods: ["GET", "POST"] });
|
||||
@@ -146,7 +146,7 @@ describe("SchemaObject", async () => {
|
||||
let called = false;
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] })
|
||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
|
||||
}),
|
||||
undefined,
|
||||
{
|
||||
@@ -155,8 +155,8 @@ describe("SchemaObject", async () => {
|
||||
called = true;
|
||||
to.methods.push("OPTIONS");
|
||||
return to;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const result = await m.set({ methods: ["GET", "POST"] });
|
||||
@@ -168,7 +168,7 @@ describe("SchemaObject", async () => {
|
||||
|
||||
test("throwIfRestricted", async () => {
|
||||
const m = new SchemaObject(Type.Object({}), undefined, {
|
||||
restrictPaths: ["a.b"]
|
||||
restrictPaths: ["a.b"],
|
||||
});
|
||||
|
||||
expect(() => m.throwIfRestricted("a.b")).toThrow();
|
||||
@@ -185,18 +185,18 @@ describe("SchemaObject", async () => {
|
||||
a: Type.String({ default: "b" }),
|
||||
b: Type.Object(
|
||||
{
|
||||
c: Type.String({ default: "d" })
|
||||
c: Type.String({ default: "d" }),
|
||||
},
|
||||
{ default: {} }
|
||||
)
|
||||
{ default: {} },
|
||||
),
|
||||
},
|
||||
{ default: {} }
|
||||
)
|
||||
{ default: {} },
|
||||
),
|
||||
}),
|
||||
undefined,
|
||||
{
|
||||
restrictPaths: ["s.b"]
|
||||
}
|
||||
restrictPaths: ["s.b"],
|
||||
},
|
||||
);
|
||||
|
||||
expect(m.patch("s.b.c", "e")).rejects.toThrow();
|
||||
@@ -217,33 +217,33 @@ describe("SchemaObject", async () => {
|
||||
additionalProperties: Type.Object({
|
||||
type: Type.String(),
|
||||
config: Type.Optional(
|
||||
Type.Object({}, { additionalProperties: Type.String() })
|
||||
)
|
||||
})
|
||||
}
|
||||
Type.Object({}, { additionalProperties: Type.String() }),
|
||||
),
|
||||
}),
|
||||
},
|
||||
),
|
||||
config: Type.Optional(Type.Object({}, { additionalProperties: Type.String() })),
|
||||
}),
|
||||
},
|
||||
),
|
||||
config: Type.Optional(Type.Object({}, { additionalProperties: Type.String() }))
|
||||
})
|
||||
}
|
||||
)
|
||||
},
|
||||
{
|
||||
additionalProperties: false
|
||||
}
|
||||
additionalProperties: false,
|
||||
},
|
||||
);
|
||||
test("patch safe object, overwrite", async () => {
|
||||
const data = {
|
||||
entities: {
|
||||
some: {
|
||||
fields: {
|
||||
a: { type: "string", config: { some: "thing" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
a: { type: "string", config: { some: "thing" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const m = new SchemaObject(dataEntitiesSchema, data, {
|
||||
forceParse: true,
|
||||
overwritePaths: [/^entities\..*\.fields\..*\.config/]
|
||||
overwritePaths: [/^entities\..*\.fields\..*\.config/],
|
||||
});
|
||||
|
||||
await m.patch("entities.some.fields.a", { type: "string", config: { another: "one" } });
|
||||
@@ -252,10 +252,10 @@ describe("SchemaObject", async () => {
|
||||
entities: {
|
||||
some: {
|
||||
fields: {
|
||||
a: { type: "string", config: { another: "one" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
a: { type: "string", config: { another: "one" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -265,22 +265,22 @@ describe("SchemaObject", async () => {
|
||||
users: {
|
||||
fields: {
|
||||
email: { type: "string" },
|
||||
password: { type: "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
password: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const m = new SchemaObject(dataEntitiesSchema, data, {
|
||||
forceParse: true,
|
||||
overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/]
|
||||
overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/],
|
||||
});
|
||||
|
||||
await m.patch("entities.test", {
|
||||
fields: {
|
||||
content: {
|
||||
type: "text"
|
||||
}
|
||||
}
|
||||
type: "text",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(m.get()).toEqual({
|
||||
@@ -288,17 +288,17 @@ describe("SchemaObject", async () => {
|
||||
users: {
|
||||
fields: {
|
||||
email: { type: "string" },
|
||||
password: { type: "string" }
|
||||
}
|
||||
password: { type: "string" },
|
||||
},
|
||||
},
|
||||
test: {
|
||||
fields: {
|
||||
content: {
|
||||
type: "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
type: "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -308,14 +308,14 @@ describe("SchemaObject", async () => {
|
||||
users: {
|
||||
fields: {
|
||||
email: { type: "string" },
|
||||
password: { type: "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
password: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const m = new SchemaObject(dataEntitiesSchema, data, {
|
||||
forceParse: true,
|
||||
overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/]
|
||||
overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/],
|
||||
});
|
||||
|
||||
expect(m.patch("desc", "entities.users.config.sort_dir")).rejects.toThrow();
|
||||
@@ -323,13 +323,13 @@ describe("SchemaObject", async () => {
|
||||
await m.patch("entities.test", {
|
||||
fields: {
|
||||
content: {
|
||||
type: "text"
|
||||
}
|
||||
}
|
||||
type: "text",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await m.patch("entities.users.config", {
|
||||
sort_dir: "desc"
|
||||
sort_dir: "desc",
|
||||
});
|
||||
|
||||
expect(m.get()).toEqual({
|
||||
@@ -337,20 +337,20 @@ describe("SchemaObject", async () => {
|
||||
users: {
|
||||
fields: {
|
||||
email: { type: "string" },
|
||||
password: { type: "string" }
|
||||
password: { type: "string" },
|
||||
},
|
||||
config: {
|
||||
sort_dir: "desc"
|
||||
}
|
||||
sort_dir: "desc",
|
||||
},
|
||||
},
|
||||
test: {
|
||||
fields: {
|
||||
content: {
|
||||
type: "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
type: "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,8 +13,8 @@ describe("diff", () => {
|
||||
t: "a",
|
||||
p: ["b"],
|
||||
o: undefined,
|
||||
n: 2
|
||||
}
|
||||
n: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -35,8 +35,8 @@ describe("diff", () => {
|
||||
t: "r",
|
||||
p: ["b"],
|
||||
o: 2,
|
||||
n: undefined
|
||||
}
|
||||
n: undefined,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -57,8 +57,8 @@ describe("diff", () => {
|
||||
t: "e",
|
||||
p: ["a"],
|
||||
o: 1,
|
||||
n: 2
|
||||
}
|
||||
n: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -79,8 +79,8 @@ describe("diff", () => {
|
||||
t: "e",
|
||||
p: ["a", "b"],
|
||||
o: 1,
|
||||
n: 2
|
||||
}
|
||||
n: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -101,14 +101,14 @@ describe("diff", () => {
|
||||
t: "e",
|
||||
p: ["a", 1],
|
||||
o: 2,
|
||||
n: 4
|
||||
n: 4,
|
||||
},
|
||||
{
|
||||
t: "a",
|
||||
p: ["a", 3],
|
||||
o: undefined,
|
||||
n: 5
|
||||
}
|
||||
n: 5,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -129,20 +129,20 @@ describe("diff", () => {
|
||||
t: "a",
|
||||
p: ["a", 0],
|
||||
o: undefined,
|
||||
n: 1
|
||||
n: 1,
|
||||
},
|
||||
{
|
||||
t: "a",
|
||||
p: ["a", 1],
|
||||
o: undefined,
|
||||
n: 2
|
||||
n: 2,
|
||||
},
|
||||
{
|
||||
t: "a",
|
||||
p: ["a", 2],
|
||||
o: undefined,
|
||||
n: 3
|
||||
}
|
||||
n: 3,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -163,14 +163,14 @@ describe("diff", () => {
|
||||
t: "e",
|
||||
p: ["a", 1],
|
||||
o: 2,
|
||||
n: 3
|
||||
n: 3,
|
||||
},
|
||||
{
|
||||
t: "r",
|
||||
p: ["a", 2],
|
||||
o: 3,
|
||||
n: undefined
|
||||
}
|
||||
n: undefined,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -183,14 +183,14 @@ describe("diff", () => {
|
||||
it("should handle complex nested changes", () => {
|
||||
const oldObj = {
|
||||
a: {
|
||||
b: [1, 2, { c: 3 }]
|
||||
}
|
||||
b: [1, 2, { c: 3 }],
|
||||
},
|
||||
};
|
||||
|
||||
const newObj = {
|
||||
a: {
|
||||
b: [1, 2, { c: 4 }, 5]
|
||||
}
|
||||
b: [1, 2, { c: 4 }, 5],
|
||||
},
|
||||
};
|
||||
|
||||
const diffs = diff(oldObj, newObj);
|
||||
@@ -200,14 +200,14 @@ describe("diff", () => {
|
||||
t: "e",
|
||||
p: ["a", "b", 2, "c"],
|
||||
o: 3,
|
||||
n: 4
|
||||
n: 4,
|
||||
},
|
||||
{
|
||||
t: "a",
|
||||
p: ["a", "b", 3],
|
||||
o: undefined,
|
||||
n: 5
|
||||
}
|
||||
n: 5,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -228,14 +228,14 @@ describe("diff", () => {
|
||||
t: "e",
|
||||
p: ["a"],
|
||||
o: undefined,
|
||||
n: null
|
||||
n: null,
|
||||
},
|
||||
{
|
||||
t: "e",
|
||||
p: ["b"],
|
||||
o: null,
|
||||
n: undefined
|
||||
}
|
||||
n: undefined,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -256,8 +256,8 @@ describe("diff", () => {
|
||||
t: "e",
|
||||
p: ["a"],
|
||||
o: 1,
|
||||
n: "1"
|
||||
}
|
||||
n: "1",
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -278,14 +278,14 @@ describe("diff", () => {
|
||||
t: "r",
|
||||
p: ["b"],
|
||||
o: 2,
|
||||
n: undefined
|
||||
n: undefined,
|
||||
},
|
||||
{
|
||||
t: "a",
|
||||
p: ["c"],
|
||||
o: undefined,
|
||||
n: 3
|
||||
}
|
||||
n: 3,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -306,8 +306,8 @@ describe("diff", () => {
|
||||
t: "e",
|
||||
p: ["a"],
|
||||
o: [1, 2, 3],
|
||||
n: { b: 4 }
|
||||
}
|
||||
n: { b: 4 },
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -328,8 +328,8 @@ describe("diff", () => {
|
||||
t: "e",
|
||||
p: ["a"],
|
||||
o: { b: 1 },
|
||||
n: 2
|
||||
}
|
||||
n: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -350,14 +350,14 @@ describe("diff", () => {
|
||||
t: "r",
|
||||
p: ["a"],
|
||||
o: 1,
|
||||
n: undefined
|
||||
n: undefined,
|
||||
},
|
||||
{
|
||||
t: "a",
|
||||
p: ["b"],
|
||||
o: undefined,
|
||||
n: 2
|
||||
}
|
||||
n: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -408,8 +408,8 @@ describe("diff", () => {
|
||||
t: "a",
|
||||
p: ["a"],
|
||||
o: undefined,
|
||||
n: 1
|
||||
}
|
||||
n: 1,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
@@ -430,8 +430,8 @@ describe("diff", () => {
|
||||
t: "r",
|
||||
p: ["a"],
|
||||
o: 1,
|
||||
n: undefined
|
||||
}
|
||||
n: undefined,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
|
||||
@@ -9,7 +9,7 @@ describe("object-query", () => {
|
||||
|
||||
test("validates", async () => {
|
||||
const converted = convert({
|
||||
name: { $eq: "ch" }
|
||||
name: { $eq: "ch" },
|
||||
});
|
||||
validate(converted, { name: "Michael" });
|
||||
});
|
||||
@@ -31,7 +31,7 @@ describe("object-query", () => {
|
||||
[{ val: { $notnull: 1 } }, { val: null }, false],
|
||||
[{ val: { $regex: ".*" } }, { val: "test" }, true],
|
||||
[{ val: { $regex: /^t.*/ } }, { val: "test" }, true],
|
||||
[{ val: { $regex: /^b.*/ } }, { val: "test" }, false]
|
||||
[{ val: { $regex: /^b.*/ } }, { val: "test" }, false],
|
||||
];
|
||||
|
||||
for (const [query, object, expected] of tests) {
|
||||
@@ -55,10 +55,10 @@ describe("object-query", () => {
|
||||
[
|
||||
{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } },
|
||||
{ val1: "foo", val2: "bar" },
|
||||
true
|
||||
true,
|
||||
],
|
||||
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, { val1: 1 }, true],
|
||||
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, { val1: 3 }, false]
|
||||
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, { val1: 3 }, false],
|
||||
];
|
||||
|
||||
for (const [query, object, expected] of tests) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Perf, isBlob, ucFirst } from "../../src/core/utils";
|
||||
import { Perf, datetimeStringUTC, isBlob, ucFirst } from "../../src/core/utils";
|
||||
import * as utils from "../../src/core/utils";
|
||||
|
||||
async function wait(ms: number) {
|
||||
@@ -16,7 +16,7 @@ describe("Core Utils", async () => {
|
||||
expect(result).toEqual([
|
||||
{ key: "a", value: 1 },
|
||||
{ key: "b", value: 2 },
|
||||
{ key: "c", value: 3 }
|
||||
{ key: "c", value: 3 },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -51,7 +51,7 @@ describe("Core Utils", async () => {
|
||||
const obj = utils.headersToObject(headers);
|
||||
expect(obj).toEqual({
|
||||
"content-type": "application/json",
|
||||
authorization: "Bearer 123"
|
||||
authorization: "Bearer 123",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -82,7 +82,7 @@ describe("Core Utils", async () => {
|
||||
file: new File([""], "file.txt"),
|
||||
stream: new ReadableStream(),
|
||||
arrayBuffer: new ArrayBuffer(10),
|
||||
arrayBufferView: new Uint8Array(new ArrayBuffer(10))
|
||||
arrayBufferView: new Uint8Array(new ArrayBuffer(10)),
|
||||
};
|
||||
|
||||
const fns = [
|
||||
@@ -90,7 +90,7 @@ describe("Core Utils", async () => {
|
||||
[utils.isBlob, "blob", ["stream", "arrayBuffer", "arrayBufferView"]],
|
||||
[utils.isFile, "file", ["stream", "arrayBuffer", "arrayBufferView"]],
|
||||
[utils.isArrayBuffer, "arrayBuffer"],
|
||||
[utils.isArrayBufferView, "arrayBufferView"]
|
||||
[utils.isArrayBufferView, "arrayBufferView"],
|
||||
] as const;
|
||||
|
||||
const additional = [0, 0.0, "", null, undefined, {}, []];
|
||||
@@ -116,10 +116,10 @@ describe("Core Utils", async () => {
|
||||
const name = "test.json";
|
||||
const text = "attachment; filename=" + name;
|
||||
const headers = new Headers({
|
||||
"Content-Disposition": text
|
||||
"Content-Disposition": text,
|
||||
});
|
||||
const request = new Request("http://example.com", {
|
||||
headers
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(utils.getContentName(text)).toBe(name);
|
||||
@@ -166,7 +166,7 @@ describe("Core Utils", async () => {
|
||||
[{ a: 1, b: 2, c: 3 }, ["b"], { a: 1, c: 3 }],
|
||||
[{ a: 1, b: 2, c: 3 }, ["c"], { a: 1, b: 2 }],
|
||||
[{ a: 1, b: 2, c: 3 }, ["a", "b"], { c: 3 }],
|
||||
[{ a: 1, b: 2, c: 3 }, ["a", "b", "c"], {}]
|
||||
[{ a: 1, b: 2, c: 3 }, ["a", "b", "c"], {}],
|
||||
] as [object, string[], object][];
|
||||
|
||||
for (const [obj, keys, expected] of objects) {
|
||||
@@ -197,9 +197,9 @@ describe("Core Utils", async () => {
|
||||
new Map([["a", 1]]),
|
||||
new Map([
|
||||
["a", 1],
|
||||
["b", 2]
|
||||
["b", 2],
|
||||
]),
|
||||
false
|
||||
false,
|
||||
],
|
||||
[{ a: 1 }, { a: 1 }, true],
|
||||
[{ a: 1 }, { a: 2 }, false],
|
||||
@@ -220,7 +220,7 @@ describe("Core Utils", async () => {
|
||||
[[1, 2, 3], [1, 2, 3, 4], false],
|
||||
[[{ a: 1 }], [{ a: 1 }], true],
|
||||
[[{ a: 1 }], [{ a: 2 }], false],
|
||||
[[{ a: 1 }], [{ b: 1 }], false]
|
||||
[[{ a: 1 }], [{ b: 1 }], false],
|
||||
] as [any, any, boolean][];
|
||||
|
||||
for (const [a, b, expected] of objects) {
|
||||
@@ -236,7 +236,7 @@ describe("Core Utils", async () => {
|
||||
[{ a: { b: 1 } }, "a.b", 1],
|
||||
[{ a: { b: 1 } }, "a.b.c", null, null],
|
||||
[{ a: { b: 1 } }, "a.b.c", 1, 1],
|
||||
[[[1]], "0.0", 1]
|
||||
[[[1]], "0.0", 1],
|
||||
] as [object, string, any, any][];
|
||||
|
||||
for (const [obj, path, expected, defaultValue] of tests) {
|
||||
@@ -245,4 +245,14 @@ describe("Core Utils", async () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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,
|
||||
type MutatorResponse,
|
||||
type RepositoryResponse,
|
||||
TextField
|
||||
TextField,
|
||||
} from "../../src/data";
|
||||
import { DataController } from "../../src/data/api/DataController";
|
||||
import { dataConfigSchema } from "../../src/data/data-schema";
|
||||
@@ -35,17 +35,17 @@ describe("[data] DataController", async () => {
|
||||
meta: {
|
||||
total: 0,
|
||||
count: 0,
|
||||
items: 0
|
||||
}
|
||||
items: 0,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
meta: {
|
||||
total: 0,
|
||||
count: 0,
|
||||
items: 0
|
||||
items: 0,
|
||||
},
|
||||
data: []
|
||||
data: [],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,22 +59,22 @@ describe("[data] DataController", async () => {
|
||||
data: [] as any,
|
||||
sql: "",
|
||||
parameters: [] as any,
|
||||
result: [] as any
|
||||
result: [] as any,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
data: []
|
||||
data: [],
|
||||
});
|
||||
});
|
||||
|
||||
describe("getController", async () => {
|
||||
const users = new Entity("users", [
|
||||
new TextField("name", { required: true }),
|
||||
new TextField("bio")
|
||||
new TextField("bio"),
|
||||
]);
|
||||
const posts = new Entity("posts", [new TextField("content")]);
|
||||
const em = new EntityManager([users, posts], dummyConnection, [
|
||||
new ManyToOneRelation(posts, users)
|
||||
new ManyToOneRelation(posts, users),
|
||||
]);
|
||||
|
||||
await em.schema().sync({ force: true });
|
||||
@@ -83,12 +83,12 @@ describe("[data] DataController", async () => {
|
||||
users: [
|
||||
{ name: "foo", bio: "bar" },
|
||||
{ name: "bar", bio: null },
|
||||
{ name: "baz", bio: "!!!" }
|
||||
{ name: "baz", bio: "!!!" },
|
||||
],
|
||||
posts: [
|
||||
{ content: "post 1", users_id: 1 },
|
||||
{ content: "post 2", users_id: 2 }
|
||||
]
|
||||
{ content: "post 2", users_id: 2 },
|
||||
],
|
||||
};
|
||||
|
||||
const ctx: any = { em, guard: new Guard() };
|
||||
@@ -118,7 +118,7 @@ describe("[data] DataController", async () => {
|
||||
for await (const _user of fixtures.users) {
|
||||
const res = await app.request("/entity/users", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(_user)
|
||||
body: JSON.stringify(_user),
|
||||
});
|
||||
//console.log("res", { _user }, res);
|
||||
const result = (await res.json()) as MutatorResponse;
|
||||
@@ -133,7 +133,7 @@ describe("[data] DataController", async () => {
|
||||
for await (const _post of fixtures.posts) {
|
||||
const res = await app.request("/entity/posts", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(_post)
|
||||
body: JSON.stringify(_post),
|
||||
});
|
||||
const result = (await res.json()) as MutatorResponse;
|
||||
const { id, ...data } = result.data as any;
|
||||
@@ -159,11 +159,11 @@ describe("[data] DataController", async () => {
|
||||
const res = await app.request("/entity/users/query", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
where: { bio: { $isnull: 1 } }
|
||||
})
|
||||
where: { bio: { $isnull: 1 } },
|
||||
}),
|
||||
});
|
||||
const data = (await res.json()) as RepositoryResponse;
|
||||
|
||||
@@ -199,7 +199,7 @@ describe("[data] DataController", async () => {
|
||||
test("/:entity (update one)", async () => {
|
||||
const res = await app.request("/entity/users/3", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ name: "new name" })
|
||||
body: JSON.stringify({ name: "new name" }),
|
||||
});
|
||||
const { data } = (await res.json()) as MutatorResponse;
|
||||
|
||||
@@ -221,7 +221,7 @@ describe("[data] DataController", async () => {
|
||||
|
||||
test("/:entity/:id (delete one)", async () => {
|
||||
const res = await app.request("/entity/posts/2", {
|
||||
method: "DELETE"
|
||||
method: "DELETE",
|
||||
});
|
||||
const { data } = (await res.json()) as RepositoryResponse<EntityData>;
|
||||
expect(data).toEqual({ id: 2, ...fixtures.posts[1] });
|
||||
|
||||
@@ -30,7 +30,7 @@ describe("data-query-impl", () => {
|
||||
[{ val: { $isnull: 0 } }, '"val" is not null', []],
|
||||
[{ val: { $isnull: false } }, '"val" is not null', []],
|
||||
[{ val: { $like: "what" } }, '"val" like ?', ["what"]],
|
||||
[{ val: { $like: "w*t" } }, '"val" like ?', ["w%t"]]
|
||||
[{ val: { $like: "w*t" } }, '"val" like ?', ["w%t"]],
|
||||
];
|
||||
|
||||
for (const [query, expectedSql, expectedParams] of tests) {
|
||||
@@ -51,22 +51,22 @@ describe("data-query-impl", () => {
|
||||
[
|
||||
{ val1: { $eq: "foo" }, val2: { $eq: "bar" } },
|
||||
'("val1" = ? and "val2" = ?)',
|
||||
["foo", "bar"]
|
||||
["foo", "bar"],
|
||||
],
|
||||
[
|
||||
{ val1: { $eq: "foo" }, val2: { $eq: "bar" } },
|
||||
'("val1" = ? and "val2" = ?)',
|
||||
["foo", "bar"]
|
||||
["foo", "bar"],
|
||||
],
|
||||
|
||||
// or constructs
|
||||
[
|
||||
{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } },
|
||||
'("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]],
|
||||
];
|
||||
|
||||
for (const [query, expectedSql, expectedParams] of tests) {
|
||||
@@ -86,7 +86,7 @@ describe("data-query-impl", () => {
|
||||
|
||||
// or constructs
|
||||
[{ $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) {
|
||||
@@ -105,23 +105,23 @@ describe("data-query-impl", () => {
|
||||
posts: {
|
||||
with: {
|
||||
images: {
|
||||
select: ["id"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
select: ["id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
with: {
|
||||
posts: {
|
||||
with: {
|
||||
images: {
|
||||
select: ["id"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
select: ["id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// over http
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
NumberField,
|
||||
PrimaryField,
|
||||
Repository,
|
||||
TextField
|
||||
TextField,
|
||||
} from "../../src/data";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
@@ -18,14 +18,14 @@ describe("some tests", async () => {
|
||||
|
||||
const users = new Entity("users", [
|
||||
new TextField("username", { required: true, default_value: "nobody" }),
|
||||
new TextField("email", { maxLength: 3 })
|
||||
new TextField("email", { maxLength: 3 }),
|
||||
]);
|
||||
|
||||
const posts = new Entity("posts", [
|
||||
new TextField("title"),
|
||||
new TextField("content"),
|
||||
new TextField("created_at"),
|
||||
new NumberField("likes", { default_value: 0 })
|
||||
new NumberField("likes", { default_value: 0 }),
|
||||
]);
|
||||
|
||||
const em = new EntityManager([users, posts], connection);
|
||||
@@ -43,7 +43,7 @@ describe("some tests", async () => {
|
||||
});*/
|
||||
|
||||
expect(query.sql).toBe(
|
||||
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" where "id" = ? limit ?'
|
||||
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" where "id" = ? limit ?',
|
||||
);
|
||||
expect(query.parameters).toEqual([1, 1]);
|
||||
expect(query.result).toEqual([]);
|
||||
@@ -53,7 +53,7 @@ describe("some tests", async () => {
|
||||
const query = await em.repository(users).findMany();
|
||||
|
||||
expect(query.sql).toBe(
|
||||
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" order by "users"."id" asc limit ? offset ?'
|
||||
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" order by "users"."id" asc limit ? offset ?',
|
||||
);
|
||||
expect(query.parameters).toEqual([10, 0]);
|
||||
expect(query.result).toEqual([]);
|
||||
@@ -63,7 +63,7 @@ describe("some tests", async () => {
|
||||
const query = await em.repository(posts).findMany();
|
||||
|
||||
expect(query.sql).toBe(
|
||||
'select "posts"."id" as "id", "posts"."title" as "title", "posts"."content" as "content", "posts"."created_at" as "created_at", "posts"."likes" as "likes" from "posts" order by "posts"."id" asc limit ? offset ?'
|
||||
'select "posts"."id" as "id", "posts"."title" as "title", "posts"."content" as "content", "posts"."created_at" as "created_at", "posts"."likes" as "likes" from "posts" order by "posts"."id" asc limit ? offset ?',
|
||||
);
|
||||
expect(query.parameters).toEqual([10, 0]);
|
||||
expect(query.result).toEqual([]);
|
||||
@@ -74,7 +74,7 @@ describe("some tests", async () => {
|
||||
new Entity("users", [
|
||||
new TextField("username"),
|
||||
new TextField("email"),
|
||||
new TextField("email") // not throwing, it's just being ignored
|
||||
new TextField("email"), // not throwing, it's just being ignored
|
||||
]);
|
||||
}).toBeDefined();
|
||||
|
||||
@@ -83,7 +83,7 @@ describe("some tests", async () => {
|
||||
new TextField("username"),
|
||||
new TextField("email"),
|
||||
// field config differs, will throw
|
||||
new TextField("email", { required: true })
|
||||
new TextField("email", { required: true }),
|
||||
]);
|
||||
}).toThrow();
|
||||
|
||||
@@ -91,7 +91,7 @@ describe("some tests", async () => {
|
||||
new Entity("users", [
|
||||
new PrimaryField(),
|
||||
new TextField("username"),
|
||||
new TextField("email")
|
||||
new TextField("email"),
|
||||
]);
|
||||
}).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ export function getDummyDatabase(memory: boolean = true): {
|
||||
afterAllCleanup: async () => {
|
||||
if (!memory) await unlink(DB_NAME);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export function getDummyConnection(memory: boolean = true) {
|
||||
|
||||
return {
|
||||
dummyConnection,
|
||||
afterAllCleanup
|
||||
afterAllCleanup,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
ManyToOneRelation,
|
||||
NumberField,
|
||||
SchemaManager,
|
||||
TextField
|
||||
TextField,
|
||||
} from "../../src/data";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
@@ -21,7 +21,7 @@ describe("Mutator relation", async () => {
|
||||
const posts = new Entity("posts", [
|
||||
new TextField("title"),
|
||||
new TextField("content", { default_value: "..." }),
|
||||
new NumberField("count", { default_value: 0 })
|
||||
new NumberField("count", { default_value: 0 }),
|
||||
]);
|
||||
|
||||
const users = new Entity("users", [new TextField("username")]);
|
||||
@@ -44,7 +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: data.id })
|
||||
em.mutator(posts).insertOne({ title: "post2", users_id: data.id }),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ describe("Mutator simple", async () => {
|
||||
|
||||
const items = new Entity("items", [
|
||||
new TextField("label", { required: true, minLength: 1 }),
|
||||
new NumberField("count", { default_value: 0 })
|
||||
new NumberField("count", { default_value: 0 }),
|
||||
]);
|
||||
const em = new EntityManager<any>([items], connection);
|
||||
|
||||
@@ -29,11 +29,11 @@ describe("Mutator simple", async () => {
|
||||
test("insert single row", async () => {
|
||||
const mutation = await em.mutator(items).insertOne({
|
||||
label: "test",
|
||||
count: 1
|
||||
count: 1,
|
||||
});
|
||||
|
||||
expect(mutation.sql).toBe(
|
||||
'insert into "items" ("count", "label") values (?, ?) returning "id", "label", "count"'
|
||||
'insert into "items" ("count", "label") values (?, ?) returning "id", "label", "count"',
|
||||
);
|
||||
expect(mutation.data).toEqual({ id: 1, label: "test", count: 1 });
|
||||
|
||||
@@ -41,8 +41,8 @@ describe("Mutator simple", async () => {
|
||||
limit: 1,
|
||||
sort: {
|
||||
by: "id",
|
||||
dir: "desc"
|
||||
}
|
||||
dir: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
expect(query.result).toEqual([{ id: 1, label: "test", count: 1 }]);
|
||||
@@ -53,18 +53,18 @@ describe("Mutator simple", async () => {
|
||||
limit: 1,
|
||||
sort: {
|
||||
by: "id",
|
||||
dir: "desc"
|
||||
}
|
||||
dir: "desc",
|
||||
},
|
||||
});
|
||||
const id = query.data![0].id as number;
|
||||
|
||||
const mutation = await em.mutator(items).updateOne(id, {
|
||||
label: "new label",
|
||||
count: 100
|
||||
count: 100,
|
||||
});
|
||||
|
||||
expect(mutation.sql).toBe(
|
||||
'update "items" set "label" = ?, "count" = ? where "id" = ? returning "id", "label", "count"'
|
||||
'update "items" set "label" = ?, "count" = ? where "id" = ? returning "id", "label", "count"',
|
||||
);
|
||||
expect(mutation.data).toEqual({ id, label: "new label", count: 100 });
|
||||
});
|
||||
@@ -74,15 +74,15 @@ describe("Mutator simple", async () => {
|
||||
limit: 1,
|
||||
sort: {
|
||||
by: "id",
|
||||
dir: "desc"
|
||||
}
|
||||
dir: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
const id = query.data![0].id as number;
|
||||
const mutation = await em.mutator(items).deleteOne(id);
|
||||
|
||||
expect(mutation.sql).toBe(
|
||||
'delete from "items" where "id" = ? returning "id", "label", "count"'
|
||||
'delete from "items" where "id" = ? returning "id", "label", "count"',
|
||||
);
|
||||
expect(mutation.data).toEqual({ id, label: "new label", count: 100 });
|
||||
|
||||
@@ -94,7 +94,7 @@ describe("Mutator simple", async () => {
|
||||
const incompleteCreate = async () =>
|
||||
await em.mutator(items).insertOne({
|
||||
//label: "test",
|
||||
count: 1
|
||||
count: 1,
|
||||
});
|
||||
|
||||
expect(incompleteCreate()).rejects.toThrow();
|
||||
@@ -104,7 +104,7 @@ describe("Mutator simple", async () => {
|
||||
const invalidCreate1 = async () =>
|
||||
await em.mutator(items).insertOne({
|
||||
label: 111, // this should work
|
||||
count: "1" // this should fail
|
||||
count: "1", // this should fail
|
||||
});
|
||||
|
||||
expect(invalidCreate1()).rejects.toThrow(TransformPersistFailedException);
|
||||
@@ -112,7 +112,7 @@ describe("Mutator simple", async () => {
|
||||
const invalidCreate2 = async () =>
|
||||
await em.mutator(items).insertOne({
|
||||
label: "", // this should fail
|
||||
count: 1
|
||||
count: 1,
|
||||
});
|
||||
|
||||
expect(invalidCreate2()).rejects.toThrow(TransformPersistFailedException);
|
||||
@@ -137,7 +137,7 @@ describe("Mutator simple", async () => {
|
||||
expect((await em.repository(items).findMany()).data.length).toBe(data.length - 2);
|
||||
//console.log((await em.repository(items).findMany()).data);
|
||||
|
||||
await em.mutator(items).deleteWhere();
|
||||
await em.mutator(items).deleteWhere({ id: { $isnull: 0 } });
|
||||
expect((await em.repository(items).findMany()).data.length).toBe(0);
|
||||
|
||||
//expect(res.data.count).toBe(0);
|
||||
@@ -152,27 +152,27 @@ describe("Mutator simple", async () => {
|
||||
await em.mutator(items).updateWhere(
|
||||
{ count: 2 },
|
||||
{
|
||||
count: 10
|
||||
}
|
||||
count: 10,
|
||||
},
|
||||
);
|
||||
expect((await em.repository(items).findMany()).data).toEqual([
|
||||
{ id: 6, label: "update", count: 1 },
|
||||
{ id: 7, label: "update too", count: 1 },
|
||||
{ id: 8, label: "keep", count: 0 }
|
||||
{ id: 8, label: "keep", count: 0 },
|
||||
]);
|
||||
|
||||
// expect 2 to be updated
|
||||
await em.mutator(items).updateWhere(
|
||||
{ count: 2 },
|
||||
{
|
||||
count: 1
|
||||
}
|
||||
count: 1,
|
||||
},
|
||||
);
|
||||
|
||||
expect((await em.repository(items).findMany()).data).toEqual([
|
||||
{ id: 6, label: "update", count: 2 },
|
||||
{ id: 7, label: "update too", count: 2 },
|
||||
{ id: 8, label: "keep", count: 0 }
|
||||
{ id: 8, label: "keep", count: 0 },
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -23,23 +23,23 @@ describe("Polymorphic", async () => {
|
||||
source: "categories",
|
||||
target: "media",
|
||||
config: {
|
||||
mappedBy: "image"
|
||||
}
|
||||
mappedBy: "image",
|
||||
},
|
||||
});
|
||||
// media should not see categories
|
||||
expect(em.relationsOf(media.name).map((r) => r.toJSON())).toEqual([]);
|
||||
|
||||
// it's important that media cannot access categories
|
||||
expect(em.relations.targetRelationsOf(categories).map((r) => r.source.entity.name)).toEqual(
|
||||
[]
|
||||
[],
|
||||
);
|
||||
expect(em.relations.targetRelationsOf(media).map((r) => r.source.entity.name)).toEqual([]);
|
||||
|
||||
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.entity.name)).toEqual([
|
||||
"media"
|
||||
"media",
|
||||
]);
|
||||
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.reference)).toEqual([
|
||||
"image"
|
||||
"image",
|
||||
]);
|
||||
expect(em.relations.sourceRelationsOf(media).map((r) => r.target.entity.name)).toEqual([]);
|
||||
|
||||
@@ -48,7 +48,7 @@ describe("Polymorphic", async () => {
|
||||
"id",
|
||||
"path",
|
||||
"reference",
|
||||
"entity_id"
|
||||
"entity_id",
|
||||
]);
|
||||
expect(media.getSelect()).toEqual(["id", "path"]);
|
||||
});
|
||||
@@ -60,7 +60,7 @@ describe("Polymorphic", async () => {
|
||||
const entities = [media, categories];
|
||||
const single = new PolymorphicRelation(categories, media, {
|
||||
mappedBy: "single",
|
||||
targetCardinality: 1
|
||||
targetCardinality: 1,
|
||||
});
|
||||
const multiple = new PolymorphicRelation(categories, media, { mappedBy: "multiple" });
|
||||
|
||||
@@ -71,17 +71,17 @@ describe("Polymorphic", async () => {
|
||||
|
||||
// it's important that media cannot access categories
|
||||
expect(em.relations.targetRelationsOf(categories).map((r) => r.source.entity.name)).toEqual(
|
||||
[]
|
||||
[],
|
||||
);
|
||||
expect(em.relations.targetRelationsOf(media).map((r) => r.source.entity.name)).toEqual([]);
|
||||
|
||||
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.entity.name)).toEqual([
|
||||
"media",
|
||||
"media"
|
||||
"media",
|
||||
]);
|
||||
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.reference)).toEqual([
|
||||
"single",
|
||||
"multiple"
|
||||
"multiple",
|
||||
]);
|
||||
expect(em.relations.sourceRelationsOf(media).map((r) => r.target.entity.name)).toEqual([]);
|
||||
|
||||
@@ -90,7 +90,7 @@ describe("Polymorphic", async () => {
|
||||
"id",
|
||||
"path",
|
||||
"reference",
|
||||
"entity_id"
|
||||
"entity_id",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
NumberField,
|
||||
OneToOneRelation,
|
||||
PolymorphicRelation,
|
||||
TextField
|
||||
TextField,
|
||||
} from "../../src/data";
|
||||
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
||||
import {
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
medium,
|
||||
number,
|
||||
relation,
|
||||
text
|
||||
text,
|
||||
} from "../../src/data/prototype";
|
||||
import { MediaField } from "../../src/media/MediaField";
|
||||
|
||||
@@ -54,7 +54,7 @@ describe("prototype", () => {
|
||||
name: text(),
|
||||
bio: text(),
|
||||
age: number(),
|
||||
some: number()
|
||||
some: number(),
|
||||
});
|
||||
type db = {
|
||||
users: Schema<typeof users>;
|
||||
@@ -70,7 +70,7 @@ describe("prototype", () => {
|
||||
name: text({ default_value: "hello" }).required(),
|
||||
bio: text(),
|
||||
age: number(),
|
||||
some: number().required()
|
||||
some: number().required(),
|
||||
});
|
||||
|
||||
const obj: InsertSchema<typeof user> = { name: "yo", some: 1 };
|
||||
@@ -83,12 +83,12 @@ describe("prototype", () => {
|
||||
new TextField("title", { required: true }),
|
||||
new TextField("content"),
|
||||
new DateField("created_at", {
|
||||
type: "datetime"
|
||||
type: "datetime",
|
||||
}),
|
||||
// @ts-ignore
|
||||
new MediaField("images", { entity: "posts" }),
|
||||
// @ts-ignore
|
||||
new MediaField("cover", { entity: "posts", max_items: 1 })
|
||||
new MediaField("cover", { entity: "posts", max_items: 1 }),
|
||||
]);
|
||||
|
||||
const posts2 = entity("posts", {
|
||||
@@ -96,7 +96,7 @@ describe("prototype", () => {
|
||||
content: text(),
|
||||
created_at: datetime(),
|
||||
images: media(),
|
||||
cover: medium()
|
||||
cover: medium(),
|
||||
});
|
||||
|
||||
type Posts = Schema<typeof posts2>;
|
||||
@@ -117,11 +117,11 @@ describe("prototype", () => {
|
||||
type: "objects",
|
||||
values: [
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "inactive", label: "Not active" }
|
||||
]
|
||||
}
|
||||
{ value: "inactive", label: "Not active" },
|
||||
],
|
||||
},
|
||||
}),
|
||||
new JsonField("json")
|
||||
new JsonField("json"),
|
||||
]);
|
||||
|
||||
const test2 = entity("test", {
|
||||
@@ -134,10 +134,10 @@ describe("prototype", () => {
|
||||
status: enumm<"active" | "inactive">({
|
||||
enum: [
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "inactive", label: "Not active" }
|
||||
]
|
||||
{ value: "inactive", label: "Not active" },
|
||||
],
|
||||
}),
|
||||
json: json<{ some: number }>()
|
||||
json: json<{ some: number }>(),
|
||||
});
|
||||
|
||||
expect(test.toJSON()).toEqual(test2.toJSON());
|
||||
@@ -161,12 +161,12 @@ describe("prototype", () => {
|
||||
// category has single image
|
||||
new PolymorphicRelation(categories, _media, {
|
||||
mappedBy: "image",
|
||||
targetCardinality: 1
|
||||
targetCardinality: 1,
|
||||
}),
|
||||
|
||||
// post has multiple images
|
||||
new PolymorphicRelation(posts, _media, { mappedBy: "images" }),
|
||||
new PolymorphicRelation(posts, _media, { mappedBy: "cover", targetCardinality: 1 })
|
||||
new PolymorphicRelation(posts, _media, { mappedBy: "cover", targetCardinality: 1 }),
|
||||
];
|
||||
|
||||
const relations2 = [
|
||||
@@ -180,7 +180,7 @@ describe("prototype", () => {
|
||||
relation(categories).polyToOne(_media, { mappedBy: "image" }),
|
||||
|
||||
relation(posts).polyToMany(_media, { mappedBy: "images" }),
|
||||
relation(posts).polyToOne(_media, { mappedBy: "cover" })
|
||||
relation(posts).polyToOne(_media, { mappedBy: "cover" }),
|
||||
];
|
||||
|
||||
expect(relations.map((r) => r.toJSON())).toEqual(relations2.map((r) => r.toJSON()));
|
||||
@@ -194,21 +194,21 @@ describe("prototype", () => {
|
||||
posts,
|
||||
categories,
|
||||
{
|
||||
connectionTableMappedName: "custom"
|
||||
connectionTableMappedName: "custom",
|
||||
},
|
||||
[new TextField("description")]
|
||||
[new TextField("description")],
|
||||
);
|
||||
|
||||
const fields = {
|
||||
description: text()
|
||||
description: text(),
|
||||
};
|
||||
let o: FieldSchema<typeof fields>;
|
||||
const rel2 = relation(posts).manyToMany(
|
||||
categories,
|
||||
{
|
||||
connectionTableMappedName: "custom"
|
||||
connectionTableMappedName: "custom",
|
||||
},
|
||||
fields
|
||||
fields,
|
||||
);
|
||||
|
||||
expect(rel.toJSON()).toEqual(rel2.toJSON());
|
||||
@@ -216,11 +216,11 @@ describe("prototype", () => {
|
||||
|
||||
test("devexample", async () => {
|
||||
const users = entity("users", {
|
||||
username: text()
|
||||
username: text(),
|
||||
});
|
||||
|
||||
const comments = entity("comments", {
|
||||
content: text()
|
||||
content: text(),
|
||||
});
|
||||
|
||||
const posts = entity("posts", {
|
||||
@@ -228,17 +228,17 @@ describe("prototype", () => {
|
||||
content: text(),
|
||||
created_at: datetime(),
|
||||
images: media(),
|
||||
cover: medium()
|
||||
cover: medium(),
|
||||
});
|
||||
|
||||
const categories = entity("categories", {
|
||||
name: text(),
|
||||
description: text(),
|
||||
image: medium()
|
||||
image: medium(),
|
||||
});
|
||||
|
||||
const settings = entity("settings", {
|
||||
theme: text()
|
||||
theme: text(),
|
||||
});
|
||||
|
||||
const test = entity("test", {
|
||||
@@ -251,10 +251,10 @@ describe("prototype", () => {
|
||||
status: enumm<"active" | "inactive">({
|
||||
enum: [
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "inactive", label: "Not active" }
|
||||
]
|
||||
{ value: "inactive", label: "Not active" },
|
||||
],
|
||||
}),
|
||||
json: json<{ some: number }>()
|
||||
json: json<{ some: number }>(),
|
||||
});
|
||||
|
||||
const _media = entity("media", {});
|
||||
@@ -270,7 +270,7 @@ describe("prototype", () => {
|
||||
relation(users).oneToOne(settings),
|
||||
|
||||
relation(comments).manyToOne(users, { required: true }),
|
||||
relation(comments).manyToOne(posts, { required: true })
|
||||
relation(comments).manyToOne(posts, { required: true }),
|
||||
];
|
||||
|
||||
const obj: Schema<typeof test> = {} as any;
|
||||
@@ -281,12 +281,12 @@ describe("prototype", () => {
|
||||
{
|
||||
posts: entity("posts", { name: text(), slug: text().required() }),
|
||||
comments: entity("comments", { some: text() }),
|
||||
users: entity("users", { email: text() })
|
||||
users: entity("users", { email: text() }),
|
||||
},
|
||||
({ relation, index }, { posts, comments, users }) => {
|
||||
relation(posts).manyToOne(comments).manyToOne(users);
|
||||
index(posts).on(["name"]).on(["slug"], true);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
type LocalDb = (typeof _em)["DB"];
|
||||
@@ -294,7 +294,7 @@ describe("prototype", () => {
|
||||
const es = [
|
||||
new Entity("posts", [new TextField("name"), new TextField("slug", { required: true })]),
|
||||
new Entity("comments", [new TextField("some")]),
|
||||
new Entity("users", [new TextField("email")])
|
||||
new Entity("users", [new TextField("email")]),
|
||||
];
|
||||
const _em2 = new EntityManager(
|
||||
es,
|
||||
@@ -302,8 +302,8 @@ describe("prototype", () => {
|
||||
[new ManyToOneRelation(es[0], es[1]), new ManyToOneRelation(es[0], es[2])],
|
||||
[
|
||||
new EntityIndex(es[0], [es[0].field("name")!]),
|
||||
new EntityIndex(es[0], [es[0].field("slug")!], true)
|
||||
]
|
||||
new EntityIndex(es[0], [es[0].field("slug")!], true),
|
||||
],
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
ManyToOneRelation,
|
||||
OneToOneRelation,
|
||||
PolymorphicRelation,
|
||||
RelationField
|
||||
RelationField,
|
||||
} from "../../src/data/relations";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
@@ -22,7 +22,7 @@ describe("Relations", async () => {
|
||||
const r1 = new RelationField("users_id", {
|
||||
reference: "users",
|
||||
target: "users",
|
||||
target_field: "id"
|
||||
target_field: "id",
|
||||
});
|
||||
|
||||
const sql1 = schema
|
||||
@@ -31,14 +31,14 @@ describe("Relations", async () => {
|
||||
.compile().sql;
|
||||
|
||||
expect(sql1).toBe(
|
||||
'create table "posts" ("users_id" integer references "users" ("id") on delete set null)'
|
||||
'create table "posts" ("users_id" integer references "users" ("id") on delete set null)',
|
||||
);
|
||||
|
||||
//const r2 = new RelationField(new Entity("users"), "author");
|
||||
const r2 = new RelationField("author_id", {
|
||||
reference: "author",
|
||||
target: "users",
|
||||
target_field: "id"
|
||||
target_field: "id",
|
||||
});
|
||||
|
||||
const sql2 = schema
|
||||
@@ -47,7 +47,7 @@ describe("Relations", async () => {
|
||||
.compile().sql;
|
||||
|
||||
expect(sql2).toBe(
|
||||
'create table "posts" ("author_id" integer references "users" ("id") on delete set null)'
|
||||
'create table "posts" ("author_id" integer references "users" ("id") on delete set null)',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -57,7 +57,7 @@ describe("Relations", async () => {
|
||||
reference: "users",
|
||||
target: "users",
|
||||
target_field: "id",
|
||||
required: true
|
||||
required: true,
|
||||
});
|
||||
expect(r1.isRequired()).toBeTrue();
|
||||
});
|
||||
@@ -66,8 +66,8 @@ describe("Relations", async () => {
|
||||
const users = new Entity("users", [new TextField("username")]);
|
||||
const posts = new Entity("posts", [
|
||||
new TextField("title", {
|
||||
maxLength: 2
|
||||
})
|
||||
maxLength: 2,
|
||||
}),
|
||||
]);
|
||||
|
||||
const entities = [users, posts];
|
||||
@@ -122,7 +122,7 @@ describe("Relations", async () => {
|
||||
.selectFrom(users.name)
|
||||
.select((eb) => postAuthorRel.buildWith(users, "posts")(eb).as("posts"));
|
||||
expect(selectPostsFromUsers.compile().sql).toBe(
|
||||
'select (select from "posts" as "posts" where "posts"."author_id" = "users"."id") as "posts" from "users"'
|
||||
'select (select from "posts" as "posts" where "posts"."author_id" = "users"."id") as "posts" from "users"',
|
||||
);
|
||||
expect(postAuthorRel!.getField()).toBeInstanceOf(RelationField);
|
||||
const userObj = { id: 1, username: "test" };
|
||||
@@ -142,7 +142,7 @@ describe("Relations", async () => {
|
||||
.select((eb) => postAuthorRel.buildWith(posts, "author")(eb).as("author"));
|
||||
|
||||
expect(selectUsersFromPosts.compile().sql).toBe(
|
||||
'select (select from "users" as "author" where "author"."id" = "posts"."author_id" limit ?) as "author" from "posts"'
|
||||
'select (select from "users" as "author" where "author"."id" = "posts"."author_id" limit ?) as "author" from "posts"',
|
||||
);
|
||||
expect(postAuthorRel.getField()).toBeInstanceOf(RelationField);
|
||||
const postObj = { id: 1, title: "test" };
|
||||
@@ -158,7 +158,7 @@ describe("Relations", async () => {
|
||||
$detach: false,
|
||||
primary: undefined,
|
||||
cardinality: undefined,
|
||||
relation_type: "n:1"
|
||||
relation_type: "n:1",
|
||||
});
|
||||
|
||||
expect(postAuthorRel!.helper(posts.name)!.getMutationInfo()).toEqual({
|
||||
@@ -170,7 +170,7 @@ describe("Relations", async () => {
|
||||
$detach: false,
|
||||
primary: "id",
|
||||
cardinality: 1,
|
||||
relation_type: "n:1"
|
||||
relation_type: "n:1",
|
||||
});
|
||||
|
||||
/*console.log("ManyToOne (source=posts, target=users)");
|
||||
@@ -225,7 +225,7 @@ describe("Relations", async () => {
|
||||
$detach: false,
|
||||
primary: "id",
|
||||
cardinality: 1,
|
||||
relation_type: "1:1"
|
||||
relation_type: "1:1",
|
||||
});
|
||||
expect(userSettingRel!.helper(settings.name)!.getMutationInfo()).toEqual({
|
||||
reference: "users",
|
||||
@@ -236,7 +236,7 @@ describe("Relations", async () => {
|
||||
$detach: false,
|
||||
primary: undefined,
|
||||
cardinality: 1,
|
||||
relation_type: "1:1"
|
||||
relation_type: "1:1",
|
||||
});
|
||||
|
||||
/*console.log("");
|
||||
@@ -310,16 +310,16 @@ describe("Relations", async () => {
|
||||
*/
|
||||
const selectCategoriesFromPosts = kysely
|
||||
.selectFrom(posts.name)
|
||||
.select((eb) => postCategoriesRel.buildWith(posts)(eb).as("categories"));
|
||||
.select((eb) => postCategoriesRel.buildWith(posts)(eb).select("id").as("categories"));
|
||||
expect(selectCategoriesFromPosts.compile().sql).toBe(
|
||||
'select (select "categories"."id" as "id", "categories"."label" as "label" from "categories" inner join "posts_categories" on "categories"."id" = "posts_categories"."categories_id" where "posts"."id" = "posts_categories"."posts_id" limit ?) as "categories" from "posts"'
|
||||
'select (select "id" from "categories" inner join "posts_categories" on "categories"."id" = "posts_categories"."categories_id" where "posts"."id" = "posts_categories"."posts_id" limit ?) as "categories" from "posts"',
|
||||
);
|
||||
|
||||
const selectPostsFromCategories = kysely
|
||||
.selectFrom(categories.name)
|
||||
.select((eb) => postCategoriesRel.buildWith(categories)(eb).as("posts"));
|
||||
.select((eb) => postCategoriesRel.buildWith(categories)(eb).select("id").as("posts"));
|
||||
expect(selectPostsFromCategories.compile().sql).toBe(
|
||||
'select (select "posts"."id" as "id", "posts"."title" as "title" from "posts" inner join "posts_categories" on "posts"."id" = "posts_categories"."posts_id" where "categories"."id" = "posts_categories"."categories_id" limit ?) as "posts" from "categories"'
|
||||
'select (select "id" from "posts" inner join "posts_categories" on "posts"."id" = "posts_categories"."posts_id" where "categories"."id" = "posts_categories"."categories_id" limit ?) as "posts" from "categories"',
|
||||
);
|
||||
|
||||
// mutation info
|
||||
@@ -332,7 +332,7 @@ describe("Relations", async () => {
|
||||
$detach: true,
|
||||
primary: "id",
|
||||
cardinality: undefined,
|
||||
relation_type: "m:n"
|
||||
relation_type: "m:n",
|
||||
});
|
||||
expect(relations[0].helper(categories.name)!.getMutationInfo()).toEqual({
|
||||
reference: "posts",
|
||||
@@ -343,7 +343,7 @@ describe("Relations", async () => {
|
||||
$detach: false,
|
||||
primary: undefined,
|
||||
cardinality: undefined,
|
||||
relation_type: "m:n"
|
||||
relation_type: "m:n",
|
||||
});
|
||||
|
||||
/*console.log("");
|
||||
|
||||
@@ -6,7 +6,7 @@ describe("[data] Entity", async () => {
|
||||
new TextField("name", { required: true }),
|
||||
new TextField("description"),
|
||||
new NumberField("age", { fillable: false, default_value: 18 }),
|
||||
new TextField("hidden", { hidden: true, default_value: "secret" })
|
||||
new TextField("hidden", { hidden: true, default_value: "secret" }),
|
||||
]);
|
||||
|
||||
test("getSelect", async () => {
|
||||
@@ -17,7 +17,7 @@ describe("[data] Entity", async () => {
|
||||
expect(entity.getFillableFields().map((f) => f.name)).toEqual([
|
||||
"name",
|
||||
"description",
|
||||
"hidden"
|
||||
"hidden",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ describe("[data] Entity", async () => {
|
||||
test("getDefaultObject", async () => {
|
||||
expect(entity.getDefaultObject()).toEqual({
|
||||
age: 18,
|
||||
hidden: "secret"
|
||||
hidden: "secret",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
EntityManager,
|
||||
ManyToManyRelation,
|
||||
ManyToOneRelation,
|
||||
SchemaManager
|
||||
SchemaManager,
|
||||
} from "../../../src/data";
|
||||
import { UnableToConnectException } from "../../../src/data/errors";
|
||||
import { getDummyConnection } from "../helper";
|
||||
@@ -25,7 +25,7 @@ describe("[data] EntityManager", async () => {
|
||||
expect(await em.ping()).toBe(true);
|
||||
expect(() => em.entity("...")).toThrow();
|
||||
expect(() =>
|
||||
em.addRelation(new ManyToOneRelation(new Entity("1"), new Entity("2")))
|
||||
em.addRelation(new ManyToOneRelation(new Entity("1"), new Entity("2"))),
|
||||
).toThrow();
|
||||
expect(em.schema()).toBeInstanceOf(SchemaManager);
|
||||
|
||||
@@ -98,7 +98,7 @@ describe("[data] EntityManager", async () => {
|
||||
expect(userTargetRel.map((r) => r.other(users).entity.name)).toEqual(["posts", "comments"]);
|
||||
expect(postTargetRel.map((r) => r.other(posts).entity.name)).toEqual([
|
||||
"comments",
|
||||
"categories"
|
||||
"categories",
|
||||
]);
|
||||
expect(commentTargetRel.map((r) => r.other(comments).entity.name)).toEqual([]);
|
||||
expect(categoriesTargetRel.map((r) => r.other(categories).entity.name)).toEqual(["posts"]);
|
||||
|
||||
@@ -12,7 +12,7 @@ describe("[data] JoinBuilder", async () => {
|
||||
const em = new EntityManager([users], dummyConnection);
|
||||
|
||||
expect(() =>
|
||||
JoinBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, ["posts"])
|
||||
JoinBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, ["posts"]),
|
||||
).toThrow('Relation "posts" not found');
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ describe("[data] JoinBuilder", async () => {
|
||||
const em = new EntityManager([users, posts], dummyConnection, relations);
|
||||
|
||||
const qb = JoinBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, [
|
||||
"posts"
|
||||
"posts",
|
||||
]);
|
||||
|
||||
const res = qb.compile();
|
||||
@@ -34,7 +34,7 @@ describe("[data] JoinBuilder", async () => {
|
||||
);*/
|
||||
|
||||
const qb2 = JoinBuilder.addClause(em, em.connection.kysely.selectFrom("posts"), posts, [
|
||||
"author"
|
||||
"author",
|
||||
]);
|
||||
|
||||
const res2 = qb2.compile();
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
OneToOneRelation,
|
||||
type RelationField,
|
||||
RelationMutator,
|
||||
TextField
|
||||
TextField,
|
||||
} from "../../../src/data";
|
||||
import * as proto from "../../../src/data/prototype";
|
||||
import { getDummyConnection } from "../helper";
|
||||
@@ -22,7 +22,7 @@ describe("[data] Mutator (base)", async () => {
|
||||
new TextField("label", { required: true }),
|
||||
new NumberField("count"),
|
||||
new TextField("hidden", { hidden: true }),
|
||||
new TextField("not_fillable", { fillable: false })
|
||||
new TextField("not_fillable", { fillable: false }),
|
||||
]);
|
||||
const em = new EntityManager<any>([entity], dummyConnection);
|
||||
await em.schema().sync({ force: true });
|
||||
@@ -44,7 +44,7 @@ describe("[data] Mutator (base)", async () => {
|
||||
test("updateOne", async () => {
|
||||
const { data } = await em.mutator(entity).insertOne(payload);
|
||||
const updated = await em.mutator(entity).updateOne(data.id, {
|
||||
count: 2
|
||||
count: 2,
|
||||
});
|
||||
|
||||
expect(updated.parameters).toEqual([2, data.id]);
|
||||
@@ -77,7 +77,7 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
||||
|
||||
// persisting relational field should just return key value to be added
|
||||
expect(
|
||||
postRelMutator.persistRelationField(postRelField, "users_id", userData.data.id)
|
||||
postRelMutator.persistRelationField(postRelField, "users_id", userData.data.id),
|
||||
).resolves.toEqual(["users_id", userData.data.id]);
|
||||
|
||||
// persisting invalid value should throw
|
||||
@@ -86,8 +86,8 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
||||
// persisting reference should ...
|
||||
expect(
|
||||
postRelMutator.persistReference(relations[0]!, "users", {
|
||||
$set: { id: userData.data.id }
|
||||
})
|
||||
$set: { id: userData.data.id },
|
||||
}),
|
||||
).resolves.toEqual(["users_id", userData.data.id]);
|
||||
// @todo: add what methods are allowed to relation, like $create should not be allowed for post<>users
|
||||
|
||||
@@ -99,8 +99,8 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
||||
expect(
|
||||
em.mutator(posts).insertOne({
|
||||
title: "post1",
|
||||
users_id: 100 // user does not exist yet
|
||||
})
|
||||
users_id: 100, // user does not exist yet
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
@@ -111,7 +111,7 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
||||
const em = new EntityManager([items, cats], dummyConnection, relations);
|
||||
|
||||
expect(em.mutator(items).insertOne({ label: "test" })).rejects.toThrow(
|
||||
'Field "cats_id" is required'
|
||||
'Field "cats_id" is required',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -119,14 +119,14 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
||||
const { data } = await em.mutator(users).insertOne({ username: "user1" });
|
||||
const res = await em.mutator(posts).insertOne({
|
||||
title: "post1",
|
||||
users_id: data.id
|
||||
users_id: data.id,
|
||||
});
|
||||
expect(res.data.users_id).toBe(data.id);
|
||||
|
||||
// setting "null" should be allowed
|
||||
const res2 = await em.mutator(posts).insertOne({
|
||||
title: "post1",
|
||||
users_id: null
|
||||
users_id: null,
|
||||
});
|
||||
expect(res2.data.users_id).toBe(null);
|
||||
});
|
||||
@@ -135,14 +135,14 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
||||
const { data } = await em.mutator(users).insertOne({ username: "user1" });
|
||||
const res = await em.mutator(posts).insertOne({
|
||||
title: "post1",
|
||||
users: { $set: { id: data.id } }
|
||||
users: { $set: { id: data.id } },
|
||||
});
|
||||
expect(res.data.users_id).toBe(data.id);
|
||||
|
||||
// setting "null" should be allowed
|
||||
const res2 = await em.mutator(posts).insertOne({
|
||||
title: "post1",
|
||||
users: { $set: { id: null } }
|
||||
users: { $set: { id: null } },
|
||||
});
|
||||
expect(res2.data.users_id).toBe(null);
|
||||
});
|
||||
@@ -151,8 +151,8 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
||||
expect(
|
||||
em.mutator(posts).insertOne({
|
||||
title: "test",
|
||||
users: { $create: { username: "test" } }
|
||||
})
|
||||
users: { $create: { username: "test" } },
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
@@ -162,27 +162,27 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
||||
const res2 = await em.mutator(posts).insertOne({ title: "post1" });
|
||||
|
||||
const up1 = await em.mutator(posts).updateOne(res2.data.id, {
|
||||
users: { $set: { id: res1.data.id } }
|
||||
users: { $set: { id: res1.data.id } },
|
||||
});
|
||||
expect(up1.data.users_id).toBe(res1.data.id);
|
||||
|
||||
const up2 = await em.mutator(posts).updateOne(res2.data.id, {
|
||||
users: { $set: { id: res1_1.data.id } }
|
||||
users: { $set: { id: res1_1.data.id } },
|
||||
});
|
||||
expect(up2.data.users_id).toBe(res1_1.data.id);
|
||||
|
||||
const up3_1 = await em.mutator(posts).updateOne(res2.data.id, {
|
||||
users_id: res1.data.id
|
||||
users_id: res1.data.id,
|
||||
});
|
||||
expect(up3_1.data.users_id).toBe(res1.data.id);
|
||||
|
||||
const up3_2 = await em.mutator(posts).updateOne(res2.data.id, {
|
||||
users_id: res1_1.data.id
|
||||
users_id: res1_1.data.id,
|
||||
});
|
||||
expect(up3_2.data.users_id).toBe(res1_1.data.id);
|
||||
|
||||
const up4 = await em.mutator(posts).updateOne(res2.data.id, {
|
||||
users_id: null
|
||||
users_id: null,
|
||||
});
|
||||
expect(up4.data.users_id).toBe(null);
|
||||
});
|
||||
@@ -199,8 +199,8 @@ describe("[data] Mutator (OneToOne)", async () => {
|
||||
expect(
|
||||
em.mutator(users).insertOne({
|
||||
username: "test",
|
||||
settings_id: 1 // todo: throws because it doesn't exist, but it shouldn't be allowed
|
||||
})
|
||||
settings_id: 1, // todo: throws because it doesn't exist, but it shouldn't be allowed
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
@@ -210,15 +210,15 @@ describe("[data] Mutator (OneToOne)", async () => {
|
||||
expect(
|
||||
em.mutator(users).insertOne({
|
||||
username: "test",
|
||||
settings: { $set: { id: data.id } }
|
||||
})
|
||||
settings: { $set: { id: data.id } },
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
test("insertOne: using $create", async () => {
|
||||
const res = await em.mutator(users).insertOne({
|
||||
username: "test",
|
||||
settings: { $create: { theme: "dark" } }
|
||||
settings: { $create: { theme: "dark" } },
|
||||
});
|
||||
expect(res.data.settings_id).toBeDefined();
|
||||
});
|
||||
@@ -303,7 +303,7 @@ describe("[data] Mutator (Events)", async () => {
|
||||
test("insertOne event return is respected", async () => {
|
||||
const posts = proto.entity("posts", {
|
||||
title: proto.text(),
|
||||
views: proto.number()
|
||||
views: proto.number(),
|
||||
});
|
||||
|
||||
const conn = getDummyConnection();
|
||||
@@ -318,10 +318,10 @@ describe("[data] Mutator (Events)", async () => {
|
||||
async (event) => {
|
||||
return {
|
||||
...event.params.data,
|
||||
views: 2
|
||||
views: 2,
|
||||
};
|
||||
},
|
||||
"sync"
|
||||
"sync",
|
||||
);
|
||||
|
||||
const mutator = em.mutator("posts");
|
||||
@@ -329,14 +329,14 @@ describe("[data] Mutator (Events)", async () => {
|
||||
expect(result.data).toEqual({
|
||||
id: 1,
|
||||
title: "test",
|
||||
views: 2
|
||||
views: 2,
|
||||
});
|
||||
});
|
||||
|
||||
test("updateOne event return is respected", async () => {
|
||||
const posts = proto.entity("posts", {
|
||||
title: proto.text(),
|
||||
views: proto.number()
|
||||
views: proto.number(),
|
||||
});
|
||||
|
||||
const conn = getDummyConnection();
|
||||
@@ -351,10 +351,10 @@ describe("[data] Mutator (Events)", async () => {
|
||||
async (event) => {
|
||||
return {
|
||||
...event.params.data,
|
||||
views: event.params.data.views + 1
|
||||
views: event.params.data.views + 1,
|
||||
};
|
||||
},
|
||||
"sync"
|
||||
"sync",
|
||||
);
|
||||
|
||||
const mutator = em.mutator("posts");
|
||||
@@ -363,7 +363,7 @@ describe("[data] Mutator (Events)", async () => {
|
||||
expect(result.data).toEqual({
|
||||
id: 1,
|
||||
title: "test",
|
||||
views: 3
|
||||
views: 3,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
LibsqlConnection,
|
||||
ManyToOneRelation,
|
||||
RepositoryEvents,
|
||||
TextField
|
||||
TextField,
|
||||
} from "../../../src/data";
|
||||
import { getDummyConnection } from "../helper";
|
||||
|
||||
@@ -70,13 +70,13 @@ describe("[Repository]", async () => {
|
||||
const q1 = selectQ(conn).compile();
|
||||
const res = await client.execute({
|
||||
sql: q1.sql,
|
||||
args: q1.parameters as any
|
||||
args: q1.parameters as any,
|
||||
});
|
||||
|
||||
const q2 = countQ(conn).compile();
|
||||
const count = await client.execute({
|
||||
sql: q2.sql,
|
||||
args: q2.parameters as any
|
||||
args: q2.parameters as any,
|
||||
});
|
||||
return [res, count];
|
||||
}
|
||||
@@ -93,7 +93,7 @@ describe("[Repository]", async () => {
|
||||
const exec = async (
|
||||
name: string,
|
||||
fn: (em: EntityManager<any>) => Promise<any>,
|
||||
em: EntityManager<any>
|
||||
em: EntityManager<any>,
|
||||
) => {
|
||||
const res = await Perf.execute(() => fn(em), times);
|
||||
await sleep(1000);
|
||||
@@ -102,7 +102,7 @@ describe("[Repository]", async () => {
|
||||
total: res.total.toFixed(2),
|
||||
avg: (res.total / times).toFixed(2),
|
||||
first: res.marks[0].time.toFixed(2),
|
||||
last: res.marks[res.marks.length - 1].time.toFixed(2)
|
||||
last: res.marks[res.marks.length - 1].time.toFixed(2),
|
||||
};
|
||||
console.log(info.name, info, res.marks);
|
||||
return info;
|
||||
@@ -183,7 +183,7 @@ describe("[data] Repository (Events)", async () => {
|
||||
const items = new Entity("items", [new TextField("label")]);
|
||||
const categories = new Entity("categories", [new TextField("label")]);
|
||||
const em = new EntityManager([items, categories], dummyConnection, [
|
||||
new ManyToOneRelation(categories, items)
|
||||
new ManyToOneRelation(categories, items),
|
||||
]);
|
||||
await em.schema().sync({ force: true });
|
||||
const events = new Map<string, any>();
|
||||
|
||||
@@ -26,7 +26,7 @@ describe("SchemaManager tests", async () => {
|
||||
isNullable: true,
|
||||
isAutoIncrementing: true,
|
||||
hasDefaultValue: false,
|
||||
comment: undefined
|
||||
comment: undefined,
|
||||
},
|
||||
{
|
||||
name: "username",
|
||||
@@ -34,7 +34,7 @@ describe("SchemaManager tests", async () => {
|
||||
isNullable: true,
|
||||
isAutoIncrementing: false,
|
||||
hasDefaultValue: false,
|
||||
comment: undefined
|
||||
comment: undefined,
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
@@ -42,7 +42,7 @@ describe("SchemaManager tests", async () => {
|
||||
isNullable: true,
|
||||
isAutoIncrementing: false,
|
||||
hasDefaultValue: false,
|
||||
comment: undefined
|
||||
comment: undefined,
|
||||
},
|
||||
{
|
||||
name: "bio",
|
||||
@@ -50,8 +50,8 @@ describe("SchemaManager tests", async () => {
|
||||
isNullable: true,
|
||||
isAutoIncrementing: false,
|
||||
hasDefaultValue: false,
|
||||
comment: undefined
|
||||
}
|
||||
comment: undefined,
|
||||
},
|
||||
],
|
||||
indices: [
|
||||
{
|
||||
@@ -61,11 +61,11 @@ describe("SchemaManager tests", async () => {
|
||||
columns: [
|
||||
{
|
||||
name: "email",
|
||||
order: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
order: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -77,10 +77,10 @@ describe("SchemaManager tests", async () => {
|
||||
new Entity(table, [
|
||||
new TextField("username"),
|
||||
new TextField("email"),
|
||||
new TextField("bio")
|
||||
])
|
||||
new TextField("bio"),
|
||||
]),
|
||||
],
|
||||
dummyConnection
|
||||
dummyConnection,
|
||||
);
|
||||
const kysely = em.connection.kysely;
|
||||
|
||||
@@ -101,8 +101,8 @@ describe("SchemaManager tests", async () => {
|
||||
name: table,
|
||||
isNew: false,
|
||||
columns: { add: ["bio"], drop: [], change: [] },
|
||||
indices: { add: [], drop: [index] }
|
||||
}
|
||||
indices: { add: [], drop: [index] },
|
||||
},
|
||||
]);
|
||||
|
||||
// now sync
|
||||
@@ -119,7 +119,7 @@ describe("SchemaManager tests", async () => {
|
||||
const table = "drop_column";
|
||||
const em = new EntityManager(
|
||||
[new Entity(table, [new TextField("username")])],
|
||||
dummyConnection
|
||||
dummyConnection,
|
||||
);
|
||||
const kysely = em.connection.kysely;
|
||||
|
||||
@@ -141,10 +141,10 @@ describe("SchemaManager tests", async () => {
|
||||
columns: {
|
||||
add: [],
|
||||
drop: ["email"],
|
||||
change: []
|
||||
change: [],
|
||||
},
|
||||
indices: { add: [], drop: [] },
|
||||
},
|
||||
indices: { add: [], drop: [] }
|
||||
}
|
||||
]);
|
||||
|
||||
// now sync
|
||||
@@ -165,15 +165,15 @@ describe("SchemaManager tests", async () => {
|
||||
new Entity(usersTable, [
|
||||
new TextField("username"),
|
||||
new TextField("email"),
|
||||
new TextField("bio")
|
||||
new TextField("bio"),
|
||||
]),
|
||||
new Entity(postsTable, [
|
||||
new TextField("title"),
|
||||
new TextField("content"),
|
||||
new TextField("created_at")
|
||||
])
|
||||
new TextField("created_at"),
|
||||
]),
|
||||
],
|
||||
dummyConnection
|
||||
dummyConnection,
|
||||
);
|
||||
const kysely = em.connection.kysely;
|
||||
|
||||
@@ -192,7 +192,7 @@ describe("SchemaManager tests", async () => {
|
||||
name: usersTable,
|
||||
isNew: false,
|
||||
columns: { add: ["bio"], drop: [], change: [] },
|
||||
indices: { add: [], drop: [] }
|
||||
indices: { add: [], drop: [] },
|
||||
},
|
||||
{
|
||||
name: postsTable,
|
||||
@@ -200,10 +200,10 @@ describe("SchemaManager tests", async () => {
|
||||
columns: {
|
||||
add: ["id", "title", "content", "created_at"],
|
||||
drop: [],
|
||||
change: []
|
||||
change: [],
|
||||
},
|
||||
indices: { add: [], drop: [] },
|
||||
},
|
||||
indices: { add: [], drop: [] }
|
||||
}
|
||||
]);
|
||||
|
||||
// now sync
|
||||
@@ -228,8 +228,8 @@ describe("SchemaManager tests", async () => {
|
||||
name: entity.name,
|
||||
isNew: true,
|
||||
columns: { add: ["id", "email"], drop: [], change: [] },
|
||||
indices: { add: [index.name!], drop: [] }
|
||||
}
|
||||
indices: { add: [index.name!], drop: [] },
|
||||
},
|
||||
]);
|
||||
|
||||
// sync and then check again
|
||||
@@ -256,8 +256,8 @@ describe("SchemaManager tests", async () => {
|
||||
name: entity.name,
|
||||
isNew: false,
|
||||
columns: { add: [], drop: [], change: [] },
|
||||
indices: { add: [index.name!], drop: [] }
|
||||
}
|
||||
indices: { add: [index.name!], drop: [] },
|
||||
},
|
||||
]);
|
||||
|
||||
// sync and then check again
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { _jsonp } from "../../../src/core/utils";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
@@ -7,10 +6,10 @@ import {
|
||||
ManyToOneRelation,
|
||||
PolymorphicRelation,
|
||||
TextField,
|
||||
WithBuilder
|
||||
WithBuilder,
|
||||
} from "../../../src/data";
|
||||
import * as proto from "../../../src/data/prototype";
|
||||
import { compileQb, prettyPrintQb, schemaToEm } from "../../helper";
|
||||
import { schemaToEm } from "../../helper";
|
||||
import { getDummyConnection } from "../helper";
|
||||
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
@@ -21,32 +20,32 @@ describe("[data] WithBuilder", async () => {
|
||||
{
|
||||
posts: proto.entity("posts", {}),
|
||||
users: proto.entity("users", {}),
|
||||
media: proto.entity("media", {})
|
||||
media: proto.entity("media", {}),
|
||||
},
|
||||
({ relation }, { posts, users, media }) => {
|
||||
relation(posts).manyToOne(users);
|
||||
relation(users).polyToOne(media, { mappedBy: "avatar" });
|
||||
}
|
||||
},
|
||||
);
|
||||
const em = schemaToEm(schema);
|
||||
|
||||
expect(WithBuilder.validateWiths(em, "posts", undefined)).toBe(0);
|
||||
expect(WithBuilder.validateWiths(em, "posts", undefined as any)).toBe(0);
|
||||
expect(WithBuilder.validateWiths(em, "posts", {})).toBe(0);
|
||||
expect(WithBuilder.validateWiths(em, "posts", { users: {} })).toBe(1);
|
||||
expect(
|
||||
WithBuilder.validateWiths(em, "posts", {
|
||||
users: {
|
||||
with: { avatar: {} }
|
||||
}
|
||||
})
|
||||
with: { avatar: {} },
|
||||
},
|
||||
}),
|
||||
).toBe(2);
|
||||
expect(() => WithBuilder.validateWiths(em, "posts", { author: {} })).toThrow();
|
||||
expect(() =>
|
||||
WithBuilder.validateWiths(em, "posts", {
|
||||
users: {
|
||||
with: { glibberish: {} }
|
||||
}
|
||||
})
|
||||
with: { glibberish: {} },
|
||||
},
|
||||
}),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
@@ -56,8 +55,8 @@ describe("[data] WithBuilder", async () => {
|
||||
|
||||
expect(() =>
|
||||
WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, {
|
||||
posts: {}
|
||||
})
|
||||
posts: {},
|
||||
}),
|
||||
).toThrow('Relation "users<>posts" not found');
|
||||
});
|
||||
|
||||
@@ -68,13 +67,13 @@ describe("[data] WithBuilder", async () => {
|
||||
const em = new EntityManager([users, posts], dummyConnection, relations);
|
||||
|
||||
const qb = WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, {
|
||||
posts: {}
|
||||
posts: {},
|
||||
});
|
||||
|
||||
const res = qb.compile();
|
||||
|
||||
expect(res.sql).toBe(
|
||||
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'content\', "agg"."content", \'author_id\', "agg"."author_id")), \'[]\') from (select "posts"."id" as "id", "posts"."content" as "content", "posts"."author_id" as "author_id" from "posts" as "posts" where "posts"."author_id" = "users"."id" order by "posts"."id" asc limit ? offset ?) as agg) as "posts" from "users"'
|
||||
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'content\', "agg"."content", \'author_id\', "agg"."author_id")), \'[]\') from (select "posts"."id" as "id", "posts"."content" as "content", "posts"."author_id" as "author_id" from "posts" as "posts" where "posts"."author_id" = "users"."id" order by "posts"."id" asc limit ? offset ?) as agg) as "posts" from "users"',
|
||||
);
|
||||
expect(res.parameters).toEqual([10, 0]);
|
||||
|
||||
@@ -83,14 +82,14 @@ describe("[data] WithBuilder", async () => {
|
||||
em.connection.kysely.selectFrom("posts"),
|
||||
posts, // @todo: try with "users", it gives output!
|
||||
{
|
||||
author: {}
|
||||
}
|
||||
author: {},
|
||||
},
|
||||
);
|
||||
|
||||
const res2 = qb2.compile();
|
||||
|
||||
expect(res2.sql).toBe(
|
||||
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "users"."id" as "id", "users"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" order by "users"."id" asc limit ? offset ?) as obj) as "author" from "posts"'
|
||||
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "users"."id" as "id", "users"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" order by "users"."id" asc limit ? offset ?) as obj) as "author" from "posts"',
|
||||
);
|
||||
expect(res2.parameters).toEqual([1, 0]);
|
||||
});
|
||||
@@ -124,7 +123,7 @@ describe("[data] WithBuilder", async () => {
|
||||
.values([
|
||||
{ posts_id: 1, categories_id: 1 },
|
||||
{ posts_id: 2, categories_id: 2 },
|
||||
{ posts_id: 1, categories_id: 2 }
|
||||
{ posts_id: 1, categories_id: 2 },
|
||||
])
|
||||
.execute();
|
||||
|
||||
@@ -138,14 +137,14 @@ describe("[data] WithBuilder", async () => {
|
||||
title: "fashion post",
|
||||
categories: [
|
||||
{ id: 1, label: "fashion" },
|
||||
{ id: 2, label: "beauty" }
|
||||
]
|
||||
{ id: 2, label: "beauty" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "beauty post",
|
||||
categories: [{ id: 2, label: "beauty" }]
|
||||
}
|
||||
categories: [{ id: 2, label: "beauty" }],
|
||||
},
|
||||
]);
|
||||
|
||||
const res2 = await em.repository(categories).findMany({ with: { posts: {} } });
|
||||
@@ -156,21 +155,21 @@ describe("[data] WithBuilder", async () => {
|
||||
{
|
||||
id: 1,
|
||||
label: "fashion",
|
||||
posts: [{ id: 1, title: "fashion post" }]
|
||||
posts: [{ id: 1, title: "fashion post" }],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: "beauty",
|
||||
posts: [
|
||||
{ id: 1, title: "fashion post" },
|
||||
{ id: 2, title: "beauty post" }
|
||||
]
|
||||
{ id: 2, title: "beauty post" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label: "tech",
|
||||
posts: []
|
||||
}
|
||||
posts: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -181,7 +180,7 @@ describe("[data] WithBuilder", async () => {
|
||||
const entities = [media, categories];
|
||||
const single = new PolymorphicRelation(categories, media, {
|
||||
mappedBy: "single",
|
||||
targetCardinality: 1
|
||||
targetCardinality: 1,
|
||||
});
|
||||
const multiple = new PolymorphicRelation(categories, media, { mappedBy: "multiple" });
|
||||
|
||||
@@ -191,11 +190,11 @@ describe("[data] WithBuilder", async () => {
|
||||
em,
|
||||
em.connection.kysely.selectFrom("categories"),
|
||||
categories,
|
||||
{ single: {} }
|
||||
{ single: {} },
|
||||
);
|
||||
const res = qb.compile();
|
||||
expect(res.sql).toBe(
|
||||
'select (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as obj) as "single" from "categories"'
|
||||
'select (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as obj) as "single" from "categories"',
|
||||
);
|
||||
expect(res.parameters).toEqual(["categories.single", 1, 0]);
|
||||
|
||||
@@ -203,11 +202,11 @@ describe("[data] WithBuilder", async () => {
|
||||
em,
|
||||
em.connection.kysely.selectFrom("categories"),
|
||||
categories,
|
||||
{ multiple: {} }
|
||||
{ multiple: {} },
|
||||
);
|
||||
const res2 = qb2.compile();
|
||||
expect(res2.sql).toBe(
|
||||
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'path\', "agg"."path")), \'[]\') from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as agg) as "multiple" from "categories"'
|
||||
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'path\', "agg"."path")), \'[]\') from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as agg) as "multiple" from "categories"',
|
||||
);
|
||||
expect(res2.parameters).toEqual(["categories.multiple", 10, 0]);
|
||||
});
|
||||
@@ -240,16 +239,16 @@ describe("[data] WithBuilder", async () => {
|
||||
{
|
||||
posts: proto.entity("posts", {}),
|
||||
users: proto.entity("users", {
|
||||
username: proto.text()
|
||||
username: proto.text(),
|
||||
}),
|
||||
media: proto.entity("media", {
|
||||
path: proto.text()
|
||||
})
|
||||
path: proto.text(),
|
||||
}),
|
||||
},
|
||||
({ relation }, { posts, users, media }) => {
|
||||
relation(posts).manyToOne(users);
|
||||
relation(users).polyToOne(media, { mappedBy: "avatar" });
|
||||
}
|
||||
},
|
||||
);
|
||||
const em = schemaToEm(schema);
|
||||
|
||||
@@ -265,16 +264,16 @@ describe("[data] WithBuilder", async () => {
|
||||
with: {
|
||||
avatar: {
|
||||
select: ["id", "path"],
|
||||
limit: 2 // ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
limit: 2, // ignored
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
//prettyPrintQb(qb);
|
||||
expect(qb.compile().sql).toBe(
|
||||
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username", \'avatar\', "obj"."avatar") from (select "users"."id" as "id", "users"."username" as "username", (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "users"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as obj) as "avatar" from "users" as "users" where "users"."id" = "posts"."users_id" order by "users"."username" asc limit ? offset ?) as obj) as "users" from "posts"'
|
||||
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username", \'avatar\', "obj"."avatar") from (select "users"."id" as "id", "users"."username" as "username", (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "users"."id" = "media"."entity_id" order by "media"."id" asc limit ? 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]);
|
||||
});
|
||||
@@ -285,17 +284,17 @@ describe("[data] WithBuilder", async () => {
|
||||
posts: proto.entity("posts", {}),
|
||||
comments: proto.entity("comments", {}),
|
||||
users: proto.entity("users", {
|
||||
username: proto.text()
|
||||
username: proto.text(),
|
||||
}),
|
||||
media: proto.entity("media", {
|
||||
path: proto.text()
|
||||
})
|
||||
path: proto.text(),
|
||||
}),
|
||||
},
|
||||
({ relation }, { posts, comments, users, media }) => {
|
||||
relation(posts).manyToOne(users).polyToOne(media, { mappedBy: "images" });
|
||||
relation(users).polyToOne(media, { mappedBy: "avatar" });
|
||||
relation(comments).manyToOne(posts).manyToOne(users);
|
||||
}
|
||||
},
|
||||
);
|
||||
const em = schemaToEm(schema);
|
||||
|
||||
@@ -308,15 +307,15 @@ describe("[data] WithBuilder", async () => {
|
||||
limit: 12,
|
||||
with: {
|
||||
users: {
|
||||
select: ["username"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
select: ["username"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(qb.compile().sql).toBe(
|
||||
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'posts_id\', "agg"."posts_id", \'users_id\', "agg"."users_id", \'users\', "agg"."users")), \'[]\') from (select "comments"."id" as "id", "comments"."posts_id" as "posts_id", "comments"."users_id" as "users_id", (select json_object(\'username\', "obj"."username") from (select "users"."username" as "username" from "users" as "users" where "users"."id" = "comments"."users_id" order by "users"."id" asc limit ? offset ?) as obj) as "users" from "comments" as "comments" where "comments"."posts_id" = "posts"."id" order by "comments"."id" asc limit ? offset ?) as agg) as "comments" from "posts"'
|
||||
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'posts_id\', "agg"."posts_id", \'users_id\', "agg"."users_id", \'users\', "agg"."users")), \'[]\') from (select "comments"."id" as "id", "comments"."posts_id" as "posts_id", "comments"."users_id" as "users_id", (select json_object(\'username\', "obj"."username") from (select "users"."username" as "username" from "users" as "users" where "users"."id" = "comments"."users_id" order by "users"."id" asc limit ? 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]);
|
||||
});
|
||||
@@ -325,23 +324,23 @@ describe("[data] WithBuilder", async () => {
|
||||
const schema = proto.em(
|
||||
{
|
||||
posts: proto.entity("posts", {
|
||||
title: proto.text()
|
||||
title: proto.text(),
|
||||
}),
|
||||
comments: proto.entity("comments", {
|
||||
content: proto.text()
|
||||
content: proto.text(),
|
||||
}),
|
||||
users: proto.entity("users", {
|
||||
username: proto.text()
|
||||
username: proto.text(),
|
||||
}),
|
||||
media: proto.entity("media", {
|
||||
path: proto.text()
|
||||
})
|
||||
path: proto.text(),
|
||||
}),
|
||||
},
|
||||
({ relation }, { posts, comments, users, media }) => {
|
||||
relation(posts).manyToOne(users).polyToOne(media, { mappedBy: "images" });
|
||||
relation(users).polyToOne(media, { mappedBy: "avatar" });
|
||||
relation(comments).manyToOne(posts).manyToOne(users);
|
||||
}
|
||||
},
|
||||
);
|
||||
const em = schemaToEm(schema);
|
||||
await em.schema().sync({ force: true });
|
||||
@@ -351,7 +350,7 @@ describe("[data] WithBuilder", async () => {
|
||||
await em.mutator("posts").insertMany([
|
||||
{ title: "post1", users_id: 1 },
|
||||
{ title: "post2", users_id: 1 },
|
||||
{ title: "post3", users_id: 2 }
|
||||
{ title: "post3", users_id: 2 },
|
||||
]);
|
||||
await em.mutator("comments").insertMany([
|
||||
{ content: "comment1", posts_id: 1, users_id: 1 },
|
||||
@@ -360,7 +359,7 @@ describe("[data] WithBuilder", async () => {
|
||||
{ content: "comment3", posts_id: 2, users_id: 1 },
|
||||
{ content: "comment4", posts_id: 2, users_id: 2 },
|
||||
{ content: "comment5", posts_id: 3, users_id: 1 },
|
||||
{ content: "comment6", posts_id: 3, users_id: 2 }
|
||||
{ content: "comment6", posts_id: 3, users_id: 2 },
|
||||
]);
|
||||
|
||||
const result = await em.repo("posts").findMany({
|
||||
@@ -371,11 +370,11 @@ describe("[data] WithBuilder", async () => {
|
||||
select: ["content"],
|
||||
with: {
|
||||
users: {
|
||||
select: ["username"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
select: ["username"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.data).toEqual([
|
||||
@@ -385,16 +384,16 @@ describe("[data] WithBuilder", async () => {
|
||||
{
|
||||
content: "comment1",
|
||||
users: {
|
||||
username: "user1"
|
||||
}
|
||||
username: "user1",
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "comment1-1",
|
||||
users: {
|
||||
username: "user1"
|
||||
}
|
||||
}
|
||||
]
|
||||
username: "user1",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "post2",
|
||||
@@ -402,16 +401,16 @@ describe("[data] WithBuilder", async () => {
|
||||
{
|
||||
content: "comment3",
|
||||
users: {
|
||||
username: "user1"
|
||||
}
|
||||
username: "user1",
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "comment4",
|
||||
users: {
|
||||
username: "user2"
|
||||
}
|
||||
}
|
||||
]
|
||||
username: "user2",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "post3",
|
||||
@@ -419,17 +418,17 @@ describe("[data] WithBuilder", async () => {
|
||||
{
|
||||
content: "comment5",
|
||||
users: {
|
||||
username: "user1"
|
||||
}
|
||||
username: "user1",
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "comment6",
|
||||
users: {
|
||||
username: "user2"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
username: "user2",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
//console.log(_jsonp(result.data));
|
||||
});
|
||||
|
||||
@@ -22,10 +22,10 @@ describe("Connection", async () => {
|
||||
columns: [
|
||||
{
|
||||
name: "name",
|
||||
order: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
order: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -54,14 +54,14 @@ describe("Connection", async () => {
|
||||
columns: [
|
||||
{
|
||||
name: "name",
|
||||
order: 0
|
||||
order: 0,
|
||||
},
|
||||
{
|
||||
name: "desc",
|
||||
order: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
order: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -83,10 +83,10 @@ describe("Connection", async () => {
|
||||
columns: [
|
||||
{
|
||||
name: "name",
|
||||
order: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
order: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,12 +10,12 @@ describe("[data] EnumField", async () => {
|
||||
runBaseFieldTests(
|
||||
EnumField,
|
||||
{ defaultValue: "a", schemaType: "text" },
|
||||
{ options: options(["a", "b", "c"]) }
|
||||
{ options: options(["a", "b", "c"]) },
|
||||
);
|
||||
|
||||
test("yields if default value is not a valid option", async () => {
|
||||
expect(
|
||||
() => new EnumField("test", { options: options(["a", "b"]), default_value: "c" })
|
||||
() => new EnumField("test", { options: options(["a", "b"]), default_value: "c" }),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ describe("[data] EnumField", async () => {
|
||||
const field = new EnumField("test", {
|
||||
options: options(["a", "b", "c"]),
|
||||
default_value: "a",
|
||||
required: true
|
||||
required: true,
|
||||
});
|
||||
|
||||
expect(field.transformRetrieve(null)).toBe("a");
|
||||
|
||||
@@ -24,20 +24,20 @@ describe("[data] Field", async () => {
|
||||
const required = new FieldSpec("test", { required: true });
|
||||
const requiredDefault = new FieldSpec("test", {
|
||||
required: true,
|
||||
default_value: "test"
|
||||
default_value: "test",
|
||||
});
|
||||
|
||||
expect(required.transformPersist(null, undefined as any, undefined as any)).rejects.toThrow();
|
||||
expect(
|
||||
required.transformPersist(undefined, undefined as any, undefined as any)
|
||||
required.transformPersist(undefined, undefined as any, undefined as any),
|
||||
).rejects.toThrow();
|
||||
|
||||
// works because it has a default value
|
||||
expect(
|
||||
requiredDefault.transformPersist(null, undefined as any, undefined as any)
|
||||
requiredDefault.transformPersist(null, undefined as any, undefined as any),
|
||||
).resolves.toBeDefined();
|
||||
expect(
|
||||
requiredDefault.transformPersist(undefined, undefined as any, undefined as any)
|
||||
requiredDefault.transformPersist(undefined, undefined as any, undefined as any),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
EntityIndex,
|
||||
type EntityManager,
|
||||
Field,
|
||||
type SchemaResponse
|
||||
type SchemaResponse,
|
||||
} from "../../../../src/data";
|
||||
|
||||
class TestField extends Field {
|
||||
|
||||
@@ -7,7 +7,7 @@ describe("[data] JsonField", async () => {
|
||||
runBaseFieldTests(JsonField, {
|
||||
defaultValue: { a: 1 },
|
||||
sampleValues: ["string", { test: 1 }, 1],
|
||||
schemaType: "text"
|
||||
schemaType: "text",
|
||||
});
|
||||
|
||||
test("transformPersist (no config)", async () => {
|
||||
|
||||
@@ -18,7 +18,7 @@ export function transformPersist(field: Field, value: any, context?: TActionCont
|
||||
export function runBaseFieldTests(
|
||||
fieldClass: ConstructableField,
|
||||
config: FieldTestConfig,
|
||||
_requiredConfig: any = {}
|
||||
_requiredConfig: any = {},
|
||||
) {
|
||||
const noConfigField = new fieldClass("no_config", _requiredConfig);
|
||||
const fillable = new fieldClass("fillable", { ..._requiredConfig, fillable: true });
|
||||
@@ -29,7 +29,7 @@ export function runBaseFieldTests(
|
||||
..._requiredConfig,
|
||||
fillable: true,
|
||||
required: true,
|
||||
default_value: config.defaultValue
|
||||
default_value: config.defaultValue,
|
||||
});
|
||||
|
||||
test("schema", () => {
|
||||
@@ -37,7 +37,7 @@ export function runBaseFieldTests(
|
||||
expect(noConfigField.schema(null as any)).toEqual([
|
||||
"no_config",
|
||||
config.schemaType,
|
||||
expect.any(Function)
|
||||
expect.any(Function),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -96,7 +96,7 @@ export function runBaseFieldTests(
|
||||
//order: 1,
|
||||
fillable: true,
|
||||
required: false,
|
||||
hidden: false
|
||||
hidden: false,
|
||||
//virtual: false,
|
||||
//default_value: undefined
|
||||
};
|
||||
@@ -105,20 +105,20 @@ export function runBaseFieldTests(
|
||||
const json = field.toJSON();
|
||||
return {
|
||||
...json,
|
||||
config: omit(json.config, ["html"])
|
||||
config: omit(json.config, ["html"]),
|
||||
};
|
||||
}
|
||||
|
||||
expect(fieldJson(noConfigField)).toEqual({
|
||||
//name: "no_config",
|
||||
type: noConfigField.type,
|
||||
config: _config
|
||||
config: _config,
|
||||
});
|
||||
|
||||
expect(fieldJson(fillable)).toEqual({
|
||||
//name: "fillable",
|
||||
type: noConfigField.type,
|
||||
config: _config
|
||||
config: _config,
|
||||
});
|
||||
|
||||
expect(fieldJson(required)).toEqual({
|
||||
@@ -126,8 +126,8 @@ export function runBaseFieldTests(
|
||||
type: required.type,
|
||||
config: {
|
||||
..._config,
|
||||
required: true
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(fieldJson(hidden)).toEqual({
|
||||
@@ -135,8 +135,8 @@ export function runBaseFieldTests(
|
||||
type: required.type,
|
||||
config: {
|
||||
..._config,
|
||||
hidden: true
|
||||
}
|
||||
hidden: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(fieldJson(dflt)).toEqual({
|
||||
@@ -144,8 +144,8 @@ export function runBaseFieldTests(
|
||||
type: dflt.type,
|
||||
config: {
|
||||
..._config,
|
||||
default_value: config.defaultValue
|
||||
}
|
||||
default_value: config.defaultValue,
|
||||
},
|
||||
});
|
||||
|
||||
expect(fieldJson(requiredAndDefault)).toEqual({
|
||||
@@ -155,8 +155,8 @@ export function runBaseFieldTests(
|
||||
..._config,
|
||||
fillable: true,
|
||||
required: true,
|
||||
default_value: config.defaultValue
|
||||
}
|
||||
default_value: config.defaultValue,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
type BaseRelationConfig,
|
||||
EntityRelation,
|
||||
EntityRelationAnchor,
|
||||
RelationTypes
|
||||
RelationTypes,
|
||||
} from "../../../../src/data/relations";
|
||||
|
||||
class TestEntityRelation extends EntityRelation {
|
||||
@@ -12,7 +12,7 @@ class TestEntityRelation extends EntityRelation {
|
||||
super(
|
||||
new EntityRelationAnchor(new Entity("source"), "source"),
|
||||
new EntityRelationAnchor(new Entity("target"), "target"),
|
||||
config
|
||||
config,
|
||||
);
|
||||
}
|
||||
initialize(em: EntityManager<any>) {}
|
||||
|
||||
@@ -30,14 +30,14 @@ beforeAll(() =>
|
||||
method: init?.method ?? "GET",
|
||||
// @ts-ignore
|
||||
headers: Object.fromEntries(init?.headers?.entries() ?? []),
|
||||
body: init?.body
|
||||
body: init?.body,
|
||||
};
|
||||
|
||||
return new Response(JSON.stringify({ todos: [1, 2], request }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
})
|
||||
}),
|
||||
);
|
||||
afterAll(unmockFetch);
|
||||
|
||||
@@ -46,7 +46,7 @@ describe("FetchTask", async () => {
|
||||
const task = new FetchTask("Fetch Something", {
|
||||
url: "https://jsonplaceholder.typicode.com/todos/1",
|
||||
method: "GET",
|
||||
headers: [{ key: "Content-Type", value: "application/json" }]
|
||||
headers: [{ key: "Content-Type", value: "application/json" }],
|
||||
});
|
||||
|
||||
const result = await task.run();
|
||||
@@ -62,18 +62,18 @@ describe("FetchTask", async () => {
|
||||
|
||||
expect(
|
||||
// // @ts-expect-error
|
||||
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: 1 })
|
||||
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: 1 }),
|
||||
).toThrow();
|
||||
|
||||
expect(
|
||||
new FetchTask("", {
|
||||
url: "https://jsonplaceholder.typicode.com",
|
||||
method: "invalid"
|
||||
}).execute()
|
||||
method: "invalid",
|
||||
}).execute(),
|
||||
).rejects.toThrow(/^Invalid method/);
|
||||
|
||||
expect(
|
||||
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: "GET" })
|
||||
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: "GET" }),
|
||||
).toBeDefined();
|
||||
|
||||
expect(() => new FetchTask("", { url: "", method: "Invalid" })).toThrow();
|
||||
@@ -85,17 +85,17 @@ describe("FetchTask", async () => {
|
||||
method: "{{ flow.output.method }}",
|
||||
headers: [
|
||||
{ key: "Content-{{ flow.output.headerKey }}", value: "application/json" },
|
||||
{ key: "Authorization", value: "Bearer {{ flow.output.apiKey }}" }
|
||||
{ key: "Authorization", value: "Bearer {{ flow.output.apiKey }}" },
|
||||
],
|
||||
body: JSON.stringify({
|
||||
email: "{{ flow.output.email }}"
|
||||
})
|
||||
email: "{{ flow.output.email }}",
|
||||
}),
|
||||
});
|
||||
const inputs = {
|
||||
headerKey: "Type",
|
||||
apiKey: 123,
|
||||
email: "what@else.com",
|
||||
method: "PATCH"
|
||||
method: "PATCH",
|
||||
};
|
||||
|
||||
const flow = new Flow("", [task]);
|
||||
|
||||
@@ -4,16 +4,16 @@ import { Flow, LogTask, RenderTask, SubFlowTask } from "../../src/flows";
|
||||
describe("SubFlowTask", async () => {
|
||||
test("Simple Subflow", async () => {
|
||||
const subTask = new RenderTask("render", {
|
||||
render: "subflow"
|
||||
render: "subflow",
|
||||
});
|
||||
const subflow = new Flow("subflow", [subTask]);
|
||||
|
||||
const task = new LogTask("log");
|
||||
const task2 = new SubFlowTask("sub", {
|
||||
flow: subflow
|
||||
flow: subflow,
|
||||
});
|
||||
const task3 = new RenderTask("render2", {
|
||||
render: "Subflow output: {{ sub.output }}"
|
||||
render: "Subflow output: {{ sub.output }}",
|
||||
});
|
||||
|
||||
const flow = new Flow("test", [task, task2, task3], []);
|
||||
@@ -30,7 +30,7 @@ describe("SubFlowTask", async () => {
|
||||
|
||||
test("Simple loop", async () => {
|
||||
const subTask = new RenderTask("render", {
|
||||
render: "run {{ flow.output }}"
|
||||
render: "run {{ flow.output }}",
|
||||
});
|
||||
const subflow = new Flow("subflow", [subTask]);
|
||||
|
||||
@@ -38,10 +38,10 @@ describe("SubFlowTask", async () => {
|
||||
const task2 = new SubFlowTask("sub", {
|
||||
flow: subflow,
|
||||
loop: true,
|
||||
input: [1, 2, 3]
|
||||
input: [1, 2, 3],
|
||||
});
|
||||
const task3 = new RenderTask("render2", {
|
||||
render: `Subflow output: {{ sub.output | join: ", " }}`
|
||||
render: `Subflow output: {{ sub.output | join: ", " }}`,
|
||||
});
|
||||
|
||||
const flow = new Flow("test", [task, task2, task3], []);
|
||||
@@ -61,7 +61,7 @@ describe("SubFlowTask", async () => {
|
||||
|
||||
test("Simple loop from flow input", async () => {
|
||||
const subTask = new RenderTask("render", {
|
||||
render: "run {{ flow.output }}"
|
||||
render: "run {{ flow.output }}",
|
||||
});
|
||||
|
||||
const subflow = new Flow("subflow", [subTask]);
|
||||
@@ -70,10 +70,10 @@ describe("SubFlowTask", async () => {
|
||||
const task2 = new SubFlowTask("sub", {
|
||||
flow: subflow,
|
||||
loop: true,
|
||||
input: "{{ flow.output | json }}"
|
||||
input: "{{ flow.output | json }}",
|
||||
});
|
||||
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], []);
|
||||
|
||||
@@ -8,13 +8,13 @@ describe("Task", async () => {
|
||||
const result = await Task.resolveParams(
|
||||
Type.Object({ test: dynamic(Type.Number()) }),
|
||||
{
|
||||
test: "{{ some.path }}"
|
||||
test: "{{ some.path }}",
|
||||
},
|
||||
{
|
||||
some: {
|
||||
path: 1
|
||||
}
|
||||
}
|
||||
path: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.test).toBe(1);
|
||||
@@ -24,13 +24,13 @@ describe("Task", async () => {
|
||||
const result = await Task.resolveParams(
|
||||
Type.Object({ test: Type.String() }),
|
||||
{
|
||||
test: "{{ some.path }}"
|
||||
test: "{{ some.path }}",
|
||||
},
|
||||
{
|
||||
some: {
|
||||
path: "1/1"
|
||||
}
|
||||
}
|
||||
path: "1/1",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.test).toBe("1/1");
|
||||
@@ -40,13 +40,13 @@ describe("Task", async () => {
|
||||
const result = await Task.resolveParams(
|
||||
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: {
|
||||
path: "1/1"
|
||||
}
|
||||
}
|
||||
path: "1/1",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.test).toEqual({ key: "path", value: "1/1" });
|
||||
@@ -55,17 +55,17 @@ describe("Task", async () => {
|
||||
test("resolveParams: with json", async () => {
|
||||
const result = await Task.resolveParams(
|
||||
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: {
|
||||
key: "path",
|
||||
value: "1/1"
|
||||
}
|
||||
}
|
||||
value: "1/1",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.test).toEqual({ key: "path", value: "1/1" });
|
||||
@@ -74,11 +74,11 @@ describe("Task", async () => {
|
||||
test("resolveParams: with array", async () => {
|
||||
const result = await Task.resolveParams(
|
||||
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"]);
|
||||
@@ -87,11 +87,11 @@ describe("Task", async () => {
|
||||
test("resolveParams: boolean", async () => {
|
||||
const result = await Task.resolveParams(
|
||||
Type.Object({
|
||||
test: dynamic(Type.Boolean())
|
||||
test: dynamic(Type.Boolean()),
|
||||
}),
|
||||
{
|
||||
test: "{{ true }}"
|
||||
}
|
||||
test: "{{ true }}",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.test).toEqual(true);
|
||||
@@ -100,11 +100,11 @@ describe("Task", async () => {
|
||||
test("resolveParams: float", async () => {
|
||||
const result = await Task.resolveParams(
|
||||
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);
|
||||
|
||||
@@ -7,11 +7,11 @@ const first = getNamedTask(
|
||||
//throw new Error("Error");
|
||||
return {
|
||||
inner: {
|
||||
result: 2
|
||||
}
|
||||
result: 2,
|
||||
},
|
||||
};
|
||||
},
|
||||
1000
|
||||
1000,
|
||||
);
|
||||
const second = getNamedTask("second (if match)");
|
||||
const third = getNamedTask("third (if error)");
|
||||
|
||||
@@ -11,7 +11,7 @@ class ExecTask extends Task {
|
||||
constructor(
|
||||
name: string,
|
||||
params: any,
|
||||
private fn: () => any
|
||||
private fn: () => any,
|
||||
) {
|
||||
super(name, params);
|
||||
}
|
||||
@@ -54,8 +54,8 @@ export function getNamedTask(name: string, _func?: () => Promise<any>, delay?: n
|
||||
return new ExecTask(
|
||||
name,
|
||||
{
|
||||
delay
|
||||
delay,
|
||||
},
|
||||
func
|
||||
func,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ const first = new LogTask("First", { delay: 1000 });
|
||||
const second = new LogTask("Second", { delay: 1000 });
|
||||
const third = new LogTask("Long Third", { delay: 2500 });
|
||||
const fourth = new FetchTask("Fetch Something", {
|
||||
url: "https://jsonplaceholder.typicode.com/todos/1"
|
||||
url: "https://jsonplaceholder.typicode.com/todos/1",
|
||||
});
|
||||
const fifth = new LogTask("Task 4", { delay: 500 }); // without connection
|
||||
|
||||
|
||||
@@ -23,10 +23,10 @@ class OutputParamTask extends Task<typeof OutputParamTask.schema> {
|
||||
static override schema = Type.Object({
|
||||
number: dynamic(
|
||||
Type.Number({
|
||||
title: "Output number"
|
||||
title: "Output number",
|
||||
}),
|
||||
Number.parseInt
|
||||
)
|
||||
Number.parseInt,
|
||||
),
|
||||
});
|
||||
|
||||
async execute(inputs: InputsMap) {
|
||||
@@ -75,7 +75,7 @@ describe("Flow task inputs", async () => {
|
||||
test("output/input", async () => {
|
||||
const task = new OutputParamTask("task1", { number: 111 });
|
||||
const task2 = new OutputParamTask("task2", {
|
||||
number: "{{ task1.output }}"
|
||||
number: "{{ task1.output }}",
|
||||
});
|
||||
|
||||
const flow = new Flow("test", [task, task2]);
|
||||
@@ -94,10 +94,10 @@ describe("Flow task inputs", async () => {
|
||||
|
||||
test("input from flow", async () => {
|
||||
const task = new OutputParamTask("task1", {
|
||||
number: "{{flow.output.someFancyParam}}"
|
||||
number: "{{flow.output.someFancyParam}}",
|
||||
});
|
||||
const task2 = new OutputParamTask("task2", {
|
||||
number: "{{task1.output}}"
|
||||
number: "{{task1.output}}",
|
||||
});
|
||||
|
||||
const flow = new Flow("test", [task, task2]);
|
||||
@@ -126,7 +126,7 @@ describe("Flow task inputs", async () => {
|
||||
const emgr = new EventManager({ EventTriggerClass });
|
||||
|
||||
const task = new OutputParamTask("event", {
|
||||
number: "{{flow.output.number}}"
|
||||
number: "{{flow.output.number}}",
|
||||
});
|
||||
const flow = new Flow(
|
||||
"test",
|
||||
@@ -134,8 +134,8 @@ describe("Flow task inputs", async () => {
|
||||
[],
|
||||
new EventTrigger({
|
||||
event: "test-event",
|
||||
mode: "sync"
|
||||
})
|
||||
mode: "sync",
|
||||
}),
|
||||
);
|
||||
flow.setRespondingTask(task);
|
||||
flow.trigger.register(flow, emgr);
|
||||
@@ -155,8 +155,8 @@ describe("Flow task inputs", async () => {
|
||||
new HttpTrigger({
|
||||
path: "/test",
|
||||
method: "GET",
|
||||
mode: "sync"
|
||||
})
|
||||
mode: "sync",
|
||||
}),
|
||||
);
|
||||
flow.setRespondingTask(task);
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ const flows = {
|
||||
back,
|
||||
fanout,
|
||||
parallel,
|
||||
simpleFetch
|
||||
simpleFetch,
|
||||
};
|
||||
|
||||
const arg = process.argv[2];
|
||||
@@ -32,7 +32,7 @@ const colors = [
|
||||
"#F78F1E", // Saffron
|
||||
"#BD10E0", // Vivid Purple
|
||||
"#50E3C2", // Turquoise
|
||||
"#9013FE" // Grape
|
||||
"#9013FE", // Grape
|
||||
];
|
||||
|
||||
const colorsCache: Record<string, string> = {};
|
||||
@@ -82,7 +82,7 @@ function TerminalFlow({ flow }: { flow: Flow }) {
|
||||
}
|
||||
|
||||
return t;
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -92,7 +92,7 @@ function TerminalFlow({ flow }: { flow: Flow }) {
|
||||
console.log("done", response ? response : "(no response)");
|
||||
console.log(
|
||||
"Executed tasks:",
|
||||
execution.logs.map((l) => l.task.name)
|
||||
execution.logs.map((l) => l.task.name),
|
||||
);
|
||||
console.log("Executed count:", execution.logs.length);
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ class ExecTask extends Task {
|
||||
constructor(
|
||||
name: string,
|
||||
params: any,
|
||||
private fn: () => any
|
||||
private fn: () => any,
|
||||
) {
|
||||
super(name, params);
|
||||
}
|
||||
@@ -60,7 +60,7 @@ describe("Flow trigger", async () => {
|
||||
"test",
|
||||
[task],
|
||||
[],
|
||||
new EventTrigger({ event: "test-event", mode: "sync" })
|
||||
new EventTrigger({ event: "test-event", mode: "sync" }),
|
||||
);
|
||||
|
||||
flow.trigger.register(flow, emgr);
|
||||
@@ -107,8 +107,8 @@ describe("Flow trigger", async () => {
|
||||
new HttpTrigger({
|
||||
path: "/test",
|
||||
method: "GET",
|
||||
mode: "sync"
|
||||
})
|
||||
mode: "sync",
|
||||
}),
|
||||
);
|
||||
|
||||
const hono = new Hono();
|
||||
@@ -123,7 +123,7 @@ describe("Flow trigger", async () => {
|
||||
|
||||
test("http trigger with response", async () => {
|
||||
const task = ExecTask.create("http", () => ({
|
||||
called: true
|
||||
called: true,
|
||||
}));
|
||||
const flow = new Flow(
|
||||
"test",
|
||||
@@ -132,8 +132,8 @@ describe("Flow trigger", async () => {
|
||||
new HttpTrigger({
|
||||
path: "/test",
|
||||
method: "GET",
|
||||
mode: "sync"
|
||||
})
|
||||
mode: "sync",
|
||||
}),
|
||||
);
|
||||
flow.setRespondingTask(task);
|
||||
|
||||
|
||||
@@ -11,13 +11,13 @@ class ExecTask extends Task<typeof ExecTask.schema> {
|
||||
type = "exec";
|
||||
|
||||
static override schema = Type.Object({
|
||||
delay: Type.Number({ default: 10 })
|
||||
delay: Type.Number({ default: 10 }),
|
||||
});
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
params: Static<typeof ExecTask.schema>,
|
||||
private func: () => Promise<any>
|
||||
private func: () => Promise<any>,
|
||||
) {
|
||||
super(name, params);
|
||||
}
|
||||
@@ -36,12 +36,12 @@ function getTask(num: number = 0, delay: number = 5) {
|
||||
return new ExecTask(
|
||||
`Task ${num}`,
|
||||
{
|
||||
delay
|
||||
delay,
|
||||
},
|
||||
async () => {
|
||||
//console.log(`[DONE] Task: ${num}`);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
//return new LogTask(`Log ${num}`, { delay });
|
||||
}
|
||||
@@ -56,9 +56,9 @@ function getNamedTask(name: string, _func?: () => Promise<any>, delay?: number)
|
||||
return new ExecTask(
|
||||
name,
|
||||
{
|
||||
delay: delay ?? 0
|
||||
delay: delay ?? 0,
|
||||
},
|
||||
func
|
||||
func,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@ describe("Flow tests", async () => {
|
||||
back
|
||||
.task(third)
|
||||
.getOutTasks()
|
||||
.map((t) => t.name)
|
||||
.map((t) => t.name),
|
||||
).toEqual(["second", "fourth"]);
|
||||
|
||||
const execution = back.createExecution();
|
||||
@@ -263,7 +263,7 @@ describe("Flow tests", async () => {
|
||||
back
|
||||
.task(third)
|
||||
.getOutTasks()
|
||||
.map((t) => t.name)
|
||||
.map((t) => t.name),
|
||||
).toEqual(["second", "fourth"]);
|
||||
|
||||
const execution = back.createExecution();
|
||||
@@ -324,8 +324,8 @@ describe("Flow tests", async () => {
|
||||
const first = getNamedTask("first", async () => {
|
||||
return {
|
||||
inner: {
|
||||
result: 2
|
||||
}
|
||||
result: 2,
|
||||
},
|
||||
};
|
||||
});
|
||||
const second = getNamedTask("second");
|
||||
@@ -361,7 +361,7 @@ describe("Flow tests", async () => {
|
||||
"[event]",
|
||||
event.isStart() ? "start" : "end",
|
||||
event.task().name,
|
||||
event.isStart() ? undefined : event.succeeded()
|
||||
event.isStart() ? undefined : event.succeeded(),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -389,7 +389,7 @@ describe("Flow tests", async () => {
|
||||
const second = new LogTask("Task 1");
|
||||
const third = new LogTask("Task 2", { delay: 50 });
|
||||
const fourth = new FetchTask("Fetch Something", {
|
||||
url: "https://jsonplaceholder.typicode.com/todos/1"
|
||||
url: "https://jsonplaceholder.typicode.com/todos/1",
|
||||
});
|
||||
const fifth = new LogTask("Task 4"); // without connection
|
||||
|
||||
@@ -405,7 +405,7 @@ describe("Flow tests", async () => {
|
||||
// @todo: fix
|
||||
const deserialized = Flow.fromObject("", original, {
|
||||
fetch: { cls: FetchTask },
|
||||
log: { cls: LogTask }
|
||||
log: { cls: LogTask },
|
||||
} as any);
|
||||
|
||||
const diffdeep = getObjectDiff(original, deserialized.toJSON());
|
||||
@@ -414,7 +414,7 @@ describe("Flow tests", async () => {
|
||||
expect(flow.startTask.name).toEqual(deserialized.startTask.name);
|
||||
expect(flow.respondingTask?.name).toEqual(
|
||||
// @ts-ignore
|
||||
deserialized.respondingTask?.name
|
||||
deserialized.respondingTask?.name,
|
||||
);
|
||||
|
||||
//console.log("--- creating original sequence");
|
||||
|
||||
@@ -4,6 +4,9 @@ import Database from "libsql";
|
||||
import { format as sqlFormat } from "sql-formatter";
|
||||
import { type Connection, EntityManager, SqliteLocalConnection } from "../src/data";
|
||||
import type { em as protoEm } from "../src/data/prototype";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { slugify } from "core/utils/strings";
|
||||
|
||||
export function getDummyDatabase(memory: boolean = true): {
|
||||
dummyDb: SqliteDatabase;
|
||||
@@ -17,7 +20,7 @@ export function getDummyDatabase(memory: boolean = true): {
|
||||
afterAllCleanup: async () => {
|
||||
if (!memory) await unlink(DB_NAME);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -27,7 +30,7 @@ export function getDummyConnection(memory: boolean = true) {
|
||||
|
||||
return {
|
||||
dummyConnection,
|
||||
afterAllCleanup
|
||||
afterAllCleanup,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,7 +42,7 @@ type ConsoleSeverity = "log" | "warn" | "error";
|
||||
const _oldConsoles = {
|
||||
log: console.log,
|
||||
warn: console.warn,
|
||||
error: console.error
|
||||
error: console.error,
|
||||
};
|
||||
|
||||
export function disableConsoleLog(severities: ConsoleSeverity[] = ["log", "warn"]) {
|
||||
@@ -71,3 +74,46 @@ export function schemaToEm(s: ReturnType<typeof protoEm>, conn?: Connection): En
|
||||
|
||||
export const assetsPath = `${import.meta.dir}/_assets`;
|
||||
export const assetsTmpPath = `${import.meta.dir}/_assets/tmp`;
|
||||
|
||||
export async function enableFetchLogging() {
|
||||
const originalFetch = global.fetch;
|
||||
|
||||
global.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const response = await originalFetch(input, init);
|
||||
const url = input instanceof URL || typeof input === "string" ? input : input.url;
|
||||
|
||||
// Only clone if it's a supported content type
|
||||
const contentType = response.headers.get("content-type") || "";
|
||||
const isSupported =
|
||||
contentType.includes("json") ||
|
||||
contentType.includes("text") ||
|
||||
contentType.includes("xml");
|
||||
|
||||
if (isSupported) {
|
||||
const clonedResponse = response.clone();
|
||||
let extension = "txt";
|
||||
let body: string;
|
||||
|
||||
if (contentType.includes("json")) {
|
||||
body = JSON.stringify(await clonedResponse.json(), null, 2);
|
||||
extension = "json";
|
||||
} else if (contentType.includes("xml")) {
|
||||
body = await clonedResponse.text();
|
||||
extension = "xml";
|
||||
} else {
|
||||
body = await clonedResponse.text();
|
||||
}
|
||||
|
||||
const fileName = `${new Date().getTime()}_${init?.method ?? "GET"}_${slugify(String(url))}.${extension}`;
|
||||
const filePath = join(assetsTmpPath, fileName);
|
||||
|
||||
await writeFile(filePath, body);
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
return () => {
|
||||
global.fetch = originalFetch;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,25 +16,25 @@ const roles = {
|
||||
"system.schema.read",
|
||||
"system.access.api",
|
||||
"system.config.read",
|
||||
"data.entity.read"
|
||||
"data.entity.read",
|
||||
],
|
||||
is_default: true
|
||||
is_default: true,
|
||||
},
|
||||
admin: {
|
||||
is_default: true,
|
||||
implicit_allow: true
|
||||
}
|
||||
implicit_allow: true,
|
||||
},
|
||||
},
|
||||
strict: {
|
||||
guest: {
|
||||
permissions: ["system.access.api", "system.config.read", "data.entity.read"],
|
||||
is_default: true
|
||||
is_default: true,
|
||||
},
|
||||
admin: {
|
||||
is_default: true,
|
||||
implicit_allow: true
|
||||
}
|
||||
}
|
||||
implicit_allow: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
const configs = {
|
||||
auth: {
|
||||
@@ -42,31 +42,31 @@ const configs = {
|
||||
entity_name: "users",
|
||||
jwt: {
|
||||
secret: secureRandomString(20),
|
||||
issuer: randomString(10)
|
||||
issuer: randomString(10),
|
||||
},
|
||||
roles: roles.strict,
|
||||
guard: {
|
||||
enabled: true
|
||||
}
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
users: {
|
||||
normal: {
|
||||
email: "normal@bknd.io",
|
||||
password: "12345678"
|
||||
password: "12345678",
|
||||
},
|
||||
admin: {
|
||||
email: "admin@bknd.io",
|
||||
password: "12345678",
|
||||
role: "admin"
|
||||
}
|
||||
}
|
||||
role: "admin",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function createAuthApp() {
|
||||
const app = createApp({
|
||||
initialConfig: {
|
||||
auth: configs.auth
|
||||
}
|
||||
auth: configs.auth,
|
||||
},
|
||||
});
|
||||
|
||||
app.emgr.onEvent(
|
||||
@@ -75,7 +75,7 @@ function createAuthApp() {
|
||||
await app.createUser(configs.users.normal);
|
||||
await app.createUser(configs.users.admin);
|
||||
},
|
||||
"sync"
|
||||
"sync",
|
||||
);
|
||||
|
||||
return app;
|
||||
@@ -94,14 +94,14 @@ const fns = <Mode extends "cookie" | "token" = "token">(app: App, mode?: Mode) =
|
||||
if (mode === "cookie") {
|
||||
return {
|
||||
cookie: `auth=${token};`,
|
||||
...additional
|
||||
...additional,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: token ? `Bearer ${token}` : "",
|
||||
"Content-Type": "application/json",
|
||||
...additional
|
||||
...additional,
|
||||
};
|
||||
}
|
||||
function body(obj?: Record<string, any>) {
|
||||
@@ -118,12 +118,12 @@ const fns = <Mode extends "cookie" | "token" = "token">(app: App, mode?: Mode) =
|
||||
|
||||
return {
|
||||
login: async (
|
||||
user: any
|
||||
user: any,
|
||||
): Promise<{ res: Response; data: Mode extends "token" ? AuthResponse : string }> => {
|
||||
const res = (await app.server.request("/api/auth/password/login", {
|
||||
method: "POST",
|
||||
headers: headers(),
|
||||
body: body(user)
|
||||
body: body(user),
|
||||
})) as Response;
|
||||
|
||||
const data = mode === "cookie" ? getCookie(res, "auth") : await res.json();
|
||||
@@ -133,10 +133,10 @@ const fns = <Mode extends "cookie" | "token" = "token">(app: App, mode?: Mode) =
|
||||
me: async (token?: string): Promise<Pick<AuthResponse, "user">> => {
|
||||
const res = (await app.server.request("/api/auth/me", {
|
||||
method: "GET",
|
||||
headers: headers(token)
|
||||
headers: headers(token),
|
||||
})) as Response;
|
||||
return await res.json();
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -219,7 +219,7 @@ describe("integration auth", () => {
|
||||
|
||||
app.server.get("/get", auth(), async (c) => {
|
||||
return c.json({
|
||||
user: c.get("auth").user ?? null
|
||||
user: c.get("auth").user ?? null,
|
||||
});
|
||||
});
|
||||
app.server.get("/wait", auth(), async (c) => {
|
||||
@@ -232,7 +232,7 @@ describe("integration auth", () => {
|
||||
expect(me.user.email).toBe(configs.users.normal.email);
|
||||
|
||||
app.server.request("/wait", {
|
||||
headers: { Authorization: `Bearer ${data.token}` }
|
||||
headers: { Authorization: `Bearer ${data.token}` },
|
||||
});
|
||||
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@ describe("integration config", () => {
|
||||
await app.build();
|
||||
const api = new Api({
|
||||
host: "http://localhost",
|
||||
fetcher: app.server.request as typeof fetch
|
||||
fetcher: app.server.request as typeof fetch,
|
||||
});
|
||||
|
||||
// create entity
|
||||
@@ -16,7 +16,7 @@ describe("integration config", () => {
|
||||
name: "posts",
|
||||
config: { sort_field: "id", sort_dir: "asc" },
|
||||
fields: { id: { type: "primary", name: "id" }, asdf: { type: "text" } },
|
||||
type: "regular"
|
||||
type: "regular",
|
||||
});
|
||||
|
||||
expect(app.em.entities.map((e) => e.name)).toContain("posts");
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { createApp, registries } from "../../src";
|
||||
import { StorageLocalAdapter } from "../../src/adapter/node";
|
||||
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(() => {
|
||||
@@ -22,13 +22,13 @@ async function makeApp(mediaOverride: Partial<TAppMediaConfig> = {}) {
|
||||
adapter: {
|
||||
type: "local",
|
||||
config: {
|
||||
path: assetsTmpPath
|
||||
}
|
||||
}
|
||||
path: assetsTmpPath,
|
||||
},
|
||||
},
|
||||
},
|
||||
mediaOverride,
|
||||
),
|
||||
},
|
||||
mediaOverride
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
await app.build();
|
||||
@@ -43,16 +43,17 @@ beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("MediaController", () => {
|
||||
test("accepts direct", async () => {
|
||||
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
|
||||
body: file,
|
||||
});
|
||||
const result = (await res.json()) as any;
|
||||
console.log(result);
|
||||
expect(result.name).toBe(name);
|
||||
|
||||
const destFile = Bun.file(assetsTmpPath + "/" + name);
|
||||
@@ -70,7 +71,7 @@ describe("MediaController", () => {
|
||||
|
||||
const res = await app.server.request("/api/media/upload/" + name, {
|
||||
method: "POST",
|
||||
body: form
|
||||
body: form,
|
||||
});
|
||||
const result = (await res.json()) as any;
|
||||
expect(result.name).toBe(name);
|
||||
@@ -87,7 +88,7 @@ describe("MediaController", () => {
|
||||
const name = makeName("png");
|
||||
const res = await app.server.request("/api/media/upload/" + name, {
|
||||
method: "POST",
|
||||
body: file
|
||||
body: file,
|
||||
});
|
||||
|
||||
expect(res.status).toBe(413);
|
||||
|
||||
@@ -60,7 +60,7 @@ describe("Storage", async () => {
|
||||
|
||||
test("uploads a file", async () => {
|
||||
const {
|
||||
meta: { type, size }
|
||||
meta: { type, size },
|
||||
} = await storage.uploadFile("hello", "world.txt");
|
||||
expect({ type, size }).toEqual({ type: "text/plain", size: 0 });
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ test("what", async () => {
|
||||
const mf = new Miniflare({
|
||||
modules: true,
|
||||
script: "export default { async fetch() { return new Response(null); } }",
|
||||
r2Buckets: ["BUCKET"]
|
||||
r2Buckets: ["BUCKET"],
|
||||
});
|
||||
|
||||
const bucket = await mf.getR2Bucket("BUCKET");
|
||||
|
||||
@@ -8,17 +8,19 @@ const {
|
||||
CLOUDINARY_CLOUD_NAME,
|
||||
CLOUDINARY_API_KEY,
|
||||
CLOUDINARY_API_SECRET,
|
||||
CLOUDINARY_UPLOAD_PRESET
|
||||
CLOUDINARY_UPLOAD_PRESET,
|
||||
} = dotenvOutput.parsed!;
|
||||
|
||||
const ALL_TESTS = !!process.env.ALL_TESTS;
|
||||
|
||||
describe.skipIf(ALL_TESTS)("StorageCloudinaryAdapter", () => {
|
||||
if (ALL_TESTS) return;
|
||||
|
||||
const adapter = new StorageCloudinaryAdapter({
|
||||
cloud_name: CLOUDINARY_CLOUD_NAME as string,
|
||||
api_key: CLOUDINARY_API_KEY as string,
|
||||
api_secret: CLOUDINARY_API_SECRET as string,
|
||||
upload_preset: CLOUDINARY_UPLOAD_PRESET as string
|
||||
upload_preset: CLOUDINARY_UPLOAD_PRESET as string,
|
||||
});
|
||||
|
||||
const file = Bun.file(`${import.meta.dir}/icon.png`);
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { randomString } from "../../../src/core/utils";
|
||||
import { StorageLocalAdapter } from "../../../src/media/storage/adapters/StorageLocalAdapter";
|
||||
import { assetsPath, assetsTmpPath } from "../../helper";
|
||||
|
||||
describe("StorageLocalAdapter", () => {
|
||||
const adapter = new StorageLocalAdapter({
|
||||
path: `${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 = `${_filename}.png`;
|
||||
|
||||
@@ -35,7 +36,7 @@ describe("StorageLocalAdapter", () => {
|
||||
test("gets object meta", async () => {
|
||||
expect(await adapter.getObjectMeta(filename)).toEqual({
|
||||
type: file.type, // image/png
|
||||
size: file.size
|
||||
size: file.size,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,34 +1,47 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { randomString } from "../../../src/core/utils";
|
||||
import { StorageS3Adapter } from "../../../src/media";
|
||||
|
||||
import { config } from "dotenv";
|
||||
//import { enableFetchLogging } from "../../helper";
|
||||
const dotenvOutput = config({ path: `${import.meta.dir}/../../../.env` });
|
||||
const { R2_ACCESS_KEY, R2_SECRET_ACCESS_KEY, R2_URL, AWS_ACCESS_KEY, AWS_SECRET_KEY, AWS_S3_URL } =
|
||||
dotenvOutput.parsed!;
|
||||
|
||||
// @todo: mock r2/s3 responses for faster tests
|
||||
const ALL_TESTS = !!process.env.ALL_TESTS;
|
||||
console.log("ALL_TESTS?", ALL_TESTS);
|
||||
|
||||
/*
|
||||
// @todo: preparation to mock s3 calls + replace fast-xml-parser
|
||||
let cleanup: () => void;
|
||||
beforeAll(async () => {
|
||||
cleanup = await enableFetchLogging();
|
||||
});
|
||||
afterAll(() => {
|
||||
cleanup();
|
||||
}); */
|
||||
|
||||
describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => {
|
||||
console.log("ALL_TESTS", process.env.ALL_TESTS);
|
||||
if (ALL_TESTS) return;
|
||||
|
||||
const versions = [
|
||||
[
|
||||
"r2",
|
||||
new StorageS3Adapter({
|
||||
access_key: R2_ACCESS_KEY as string,
|
||||
secret_access_key: R2_SECRET_ACCESS_KEY as string,
|
||||
url: R2_URL as string
|
||||
})
|
||||
url: R2_URL as string,
|
||||
}),
|
||||
],
|
||||
[
|
||||
"s3",
|
||||
new StorageS3Adapter({
|
||||
access_key: AWS_ACCESS_KEY as string,
|
||||
secret_access_key: AWS_SECRET_KEY as string,
|
||||
url: AWS_S3_URL as string
|
||||
})
|
||||
]
|
||||
url: AWS_S3_URL as string,
|
||||
}),
|
||||
],
|
||||
] as const;
|
||||
|
||||
const _conf = {
|
||||
@@ -39,8 +52,8 @@ describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => {
|
||||
"objectExists",
|
||||
"getObject",
|
||||
"deleteObject",
|
||||
"getObjectMeta"
|
||||
]
|
||||
"getObjectMeta",
|
||||
],
|
||||
};
|
||||
|
||||
const file = Bun.file(`${import.meta.dir}/icon.png`);
|
||||
@@ -55,7 +68,7 @@ describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => {
|
||||
|
||||
// @todo: add mocked fetch for faster tests
|
||||
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);
|
||||
return;
|
||||
}
|
||||
@@ -64,7 +77,7 @@ describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => {
|
||||
|
||||
test.skipIf(disabled("putObject"))("puts an object", async () => {
|
||||
objects = (await adapter.listObjects()).length;
|
||||
expect(await adapter.putObject(filename, file)).toBeString();
|
||||
expect(await adapter.putObject(filename, file as any)).toBeString();
|
||||
});
|
||||
|
||||
test.skipIf(disabled("listObjects"))("lists objects", async () => {
|
||||
@@ -84,7 +97,7 @@ describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => {
|
||||
test.skipIf(disabled("getObjectMeta"))("gets object meta", async () => {
|
||||
expect(await adapter.getObjectMeta(filename)).toEqual({
|
||||
type: file.type, // image/png
|
||||
size: file.size
|
||||
size: file.size,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import * as large from "../../src/media/storage/mime-types";
|
||||
import * as tiny from "../../src/media/storage/mime-types-tiny";
|
||||
import { getRandomizedFilename } from "../../src/media/utils";
|
||||
|
||||
describe("media/mime-types", () => {
|
||||
test("tiny resolves", () => {
|
||||
@@ -27,7 +28,7 @@ describe("media/mime-types", () => {
|
||||
exts,
|
||||
ext,
|
||||
expected: ex,
|
||||
actual: large.guessMimeType("." + ext)
|
||||
actual: large.guessMimeType("." + ext),
|
||||
});
|
||||
throw new Error(`Failed for ${ext}`);
|
||||
}
|
||||
@@ -36,19 +37,62 @@ describe("media/mime-types", () => {
|
||||
});
|
||||
|
||||
test("isMimeType", () => {
|
||||
expect(tiny.isMimeType("image/jpeg")).toBe(true);
|
||||
expect(tiny.isMimeType("image/jpeg", ["image/png"])).toBe(true);
|
||||
expect(tiny.isMimeType("image/png", ["image/png"])).toBe(false);
|
||||
expect(tiny.isMimeType("image/png")).toBe(true);
|
||||
expect(tiny.isMimeType("whatever")).toBe(false);
|
||||
expect(tiny.isMimeType("text/tab-separated-values")).toBe(true);
|
||||
const tests = [
|
||||
["image/avif", true],
|
||||
["image/AVIF", true],
|
||||
["image/jpeg", true],
|
||||
["image/jpeg", true, ["image/png"]],
|
||||
["image/png", false, ["image/png"]],
|
||||
["image/png", true],
|
||||
["image/heif", true],
|
||||
["image/heic", true],
|
||||
["image/gif", true],
|
||||
["whatever", false],
|
||||
["text/tab-separated-values", true],
|
||||
["application/zip", true],
|
||||
];
|
||||
|
||||
for (const [mime, expected, exclude] of tests) {
|
||||
expect(
|
||||
tiny.isMimeType(mime, exclude as any),
|
||||
`isMimeType(): ${mime} should be ${expected}`,
|
||||
).toBe(expected as any);
|
||||
}
|
||||
});
|
||||
|
||||
test("extension", () => {
|
||||
expect(tiny.extension("image/png")).toBe("png");
|
||||
expect(tiny.extension("image/jpeg")).toBe("jpeg");
|
||||
expect(tiny.extension("application/zip")).toBe("zip");
|
||||
expect(tiny.extension("text/tab-separated-values")).toBe("tsv");
|
||||
expect(tiny.extension("application/zip")).toBe("zip");
|
||||
const tests = [
|
||||
["image/avif", "avif"],
|
||||
["image/png", "png"],
|
||||
["image/PNG", "png"],
|
||||
["image/jpeg", "jpeg"],
|
||||
["application/zip", "zip"],
|
||||
["text/tab-separated-values", "tsv"],
|
||||
["application/zip", "zip"],
|
||||
];
|
||||
|
||||
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,
|
||||
jwt: {
|
||||
secret: "123456"
|
||||
}
|
||||
secret: "123456",
|
||||
},
|
||||
ctx
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
|
||||
await auth.build();
|
||||
@@ -63,12 +63,12 @@ describe("AppAuth", () => {
|
||||
const res = await app.request("/password/register", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: "some@body.com",
|
||||
password: "123456"
|
||||
})
|
||||
password: "123456",
|
||||
}),
|
||||
});
|
||||
enableConsoleLog();
|
||||
expect(res.status).toBe(200);
|
||||
@@ -85,10 +85,10 @@ describe("AppAuth", () => {
|
||||
auth: {
|
||||
enabled: true,
|
||||
jwt: {
|
||||
secret: "123456"
|
||||
}
|
||||
}
|
||||
}
|
||||
secret: "123456",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await app.build();
|
||||
@@ -109,14 +109,14 @@ describe("AppAuth", () => {
|
||||
initialConfig: {
|
||||
auth: {
|
||||
entity_name: "users",
|
||||
enabled: true
|
||||
enabled: true,
|
||||
},
|
||||
data: em({
|
||||
users: entity("users", {
|
||||
additional: text()
|
||||
})
|
||||
}).toJSON()
|
||||
}
|
||||
additional: text(),
|
||||
}),
|
||||
}).toJSON(),
|
||||
},
|
||||
});
|
||||
|
||||
await app.build();
|
||||
@@ -132,21 +132,21 @@ describe("AppAuth", () => {
|
||||
const app = createApp({
|
||||
initialConfig: {
|
||||
auth: {
|
||||
enabled: true
|
||||
enabled: true,
|
||||
},
|
||||
data: em({
|
||||
users: entity("users", {
|
||||
strategy: text({
|
||||
fillable: true,
|
||||
hidden: false
|
||||
hidden: false,
|
||||
}),
|
||||
strategy_value: text({
|
||||
fillable: true,
|
||||
hidden: false
|
||||
})
|
||||
})
|
||||
}).toJSON()
|
||||
}
|
||||
hidden: false,
|
||||
}),
|
||||
}),
|
||||
}).toJSON(),
|
||||
},
|
||||
});
|
||||
await app.build();
|
||||
|
||||
@@ -157,7 +157,7 @@ describe("AppAuth", () => {
|
||||
const authField = make(name, _authFieldProto as any);
|
||||
const field = users.field(name)!;
|
||||
for (const prop of props) {
|
||||
expect(field.config[prop]).toBe(authField.config[prop]);
|
||||
expect(field.config[prop]).toEqual(authField.config[prop]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -19,16 +19,16 @@ describe("AppMedia", () => {
|
||||
adapter: {
|
||||
type: "local",
|
||||
config: {
|
||||
path: "./"
|
||||
}
|
||||
}
|
||||
path: "./",
|
||||
},
|
||||
},
|
||||
},
|
||||
data: em({
|
||||
media: entity("media", {
|
||||
additional: text()
|
||||
})
|
||||
}).toJSON()
|
||||
}
|
||||
additional: text(),
|
||||
}),
|
||||
}).toJSON(),
|
||||
},
|
||||
});
|
||||
|
||||
await app.build();
|
||||
@@ -49,7 +49,7 @@ describe("AppMedia", () => {
|
||||
"modified_at",
|
||||
"reference",
|
||||
"entity_id",
|
||||
"metadata"
|
||||
"metadata",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,7 +47,7 @@ describe("Module", async () => {
|
||||
prt = {
|
||||
ensureEntity: this.ensureEntity.bind(this),
|
||||
ensureIndex: this.ensureIndex.bind(this),
|
||||
ensureSchema: this.ensureSchema.bind(this)
|
||||
ensureSchema: this.ensureSchema.bind(this),
|
||||
};
|
||||
|
||||
get em() {
|
||||
@@ -60,7 +60,7 @@ describe("Module", async () => {
|
||||
Object.values(_em.entities),
|
||||
new DummyConnection(),
|
||||
_em.relations,
|
||||
_em.indices
|
||||
_em.indices,
|
||||
);
|
||||
return new M({} as any, { em, flags: Module.ctx_flags } as any);
|
||||
}
|
||||
@@ -69,14 +69,14 @@ describe("Module", async () => {
|
||||
entities: _em.entities.map((e) => ({
|
||||
name: e.name,
|
||||
fields: e.fields.map((f) => f.name),
|
||||
type: e.type
|
||||
type: e.type,
|
||||
})),
|
||||
indices: _em.indices.map((i) => ({
|
||||
name: i.name,
|
||||
entity: i.entity.name,
|
||||
fields: i.fields.map((f) => f.name),
|
||||
unique: i.unique
|
||||
}))
|
||||
unique: i.unique,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -88,15 +88,15 @@ describe("Module", async () => {
|
||||
|
||||
expect(flat(make(initial).em)).toEqual({
|
||||
entities: [],
|
||||
indices: []
|
||||
indices: [],
|
||||
});
|
||||
});
|
||||
|
||||
test("init", () => {
|
||||
const initial = em({
|
||||
users: entity("u", {
|
||||
name: text()
|
||||
})
|
||||
name: text(),
|
||||
}),
|
||||
});
|
||||
|
||||
const m = make(initial);
|
||||
@@ -107,18 +107,18 @@ describe("Module", async () => {
|
||||
{
|
||||
name: "u",
|
||||
fields: ["id", "name"],
|
||||
type: "regular"
|
||||
}
|
||||
type: "regular",
|
||||
},
|
||||
],
|
||||
indices: []
|
||||
indices: [],
|
||||
});
|
||||
});
|
||||
|
||||
test("ensure entity", () => {
|
||||
const initial = em({
|
||||
users: entity("u", {
|
||||
name: text()
|
||||
})
|
||||
name: text(),
|
||||
}),
|
||||
});
|
||||
|
||||
const m = make(initial);
|
||||
@@ -127,17 +127,17 @@ describe("Module", async () => {
|
||||
{
|
||||
name: "u",
|
||||
fields: ["id", "name"],
|
||||
type: "regular"
|
||||
}
|
||||
type: "regular",
|
||||
},
|
||||
],
|
||||
indices: []
|
||||
indices: [],
|
||||
});
|
||||
|
||||
// this should add a new entity
|
||||
m.prt.ensureEntity(
|
||||
entity("p", {
|
||||
title: text()
|
||||
})
|
||||
title: text(),
|
||||
}),
|
||||
);
|
||||
|
||||
// this should only add the field "important"
|
||||
@@ -145,11 +145,11 @@ describe("Module", async () => {
|
||||
entity(
|
||||
"u",
|
||||
{
|
||||
important: text()
|
||||
important: text(),
|
||||
},
|
||||
undefined,
|
||||
"system"
|
||||
)
|
||||
"system",
|
||||
),
|
||||
);
|
||||
|
||||
expect(m.ctx.flags.sync_required).toBe(true);
|
||||
@@ -159,22 +159,22 @@ describe("Module", async () => {
|
||||
name: "u",
|
||||
fields: ["id", "name", "important"],
|
||||
// ensured type must be present
|
||||
type: "system"
|
||||
type: "system",
|
||||
},
|
||||
{
|
||||
name: "p",
|
||||
fields: ["id", "title"],
|
||||
type: "regular"
|
||||
}
|
||||
type: "regular",
|
||||
},
|
||||
],
|
||||
indices: []
|
||||
indices: [],
|
||||
});
|
||||
});
|
||||
|
||||
test("ensure index", () => {
|
||||
const users = entity("u", {
|
||||
name: text(),
|
||||
title: text()
|
||||
title: text(),
|
||||
});
|
||||
const initial = em({ users }, ({ index }, { users }) => {
|
||||
index(users).on(["title"]);
|
||||
@@ -189,23 +189,23 @@ describe("Module", async () => {
|
||||
{
|
||||
name: "u",
|
||||
fields: ["id", "name", "title"],
|
||||
type: "regular"
|
||||
}
|
||||
type: "regular",
|
||||
},
|
||||
],
|
||||
indices: [
|
||||
{
|
||||
name: "idx_u_title",
|
||||
entity: "u",
|
||||
fields: ["title"],
|
||||
unique: false
|
||||
unique: false,
|
||||
},
|
||||
{
|
||||
name: "idx_u_name",
|
||||
entity: "u",
|
||||
fields: ["name"],
|
||||
unique: false
|
||||
}
|
||||
]
|
||||
unique: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,10 +39,10 @@ describe("ModuleManager", async () => {
|
||||
basepath: "/api/data2",
|
||||
entities: {
|
||||
test: entity("test", {
|
||||
content: text()
|
||||
}).toJSON()
|
||||
}
|
||||
}
|
||||
content: text(),
|
||||
}).toJSON(),
|
||||
},
|
||||
},
|
||||
});
|
||||
//const { version, ...json } = mm.toJSON() as any;
|
||||
|
||||
@@ -69,10 +69,10 @@ describe("ModuleManager", async () => {
|
||||
basepath: "/api/data2",
|
||||
entities: {
|
||||
test: entity("test", {
|
||||
content: text()
|
||||
}).toJSON()
|
||||
}
|
||||
}
|
||||
content: text(),
|
||||
}).toJSON(),
|
||||
},
|
||||
},
|
||||
};
|
||||
//const { version, ...json } = mm.toJSON() as any;
|
||||
|
||||
@@ -105,7 +105,7 @@ describe("ModuleManager", async () => {
|
||||
const c2 = getDummyConnection();
|
||||
const db = c2.dummyConnection.kysely;
|
||||
const mm2 = new ModuleManager(c2.dummyConnection, {
|
||||
initial: { version: version - 1, ...json }
|
||||
initial: { version: version - 1, ...json },
|
||||
});
|
||||
await mm2.syncConfigTable();
|
||||
|
||||
@@ -129,7 +129,7 @@ describe("ModuleManager", async () => {
|
||||
const db = c2.dummyConnection.kysely;
|
||||
|
||||
const mm2 = new ModuleManager(c2.dummyConnection, {
|
||||
initial: { version: version - 1, ...json }
|
||||
initial: { version: version - 1, ...json },
|
||||
});
|
||||
await mm2.syncConfigTable();
|
||||
await db
|
||||
@@ -157,8 +157,8 @@ describe("ModuleManager", async () => {
|
||||
...json,
|
||||
data: {
|
||||
...json.data,
|
||||
basepath: "/api/data2"
|
||||
}
|
||||
basepath: "/api/data2",
|
||||
},
|
||||
};
|
||||
await db
|
||||
.insertInto(TABLE_NAME)
|
||||
@@ -190,9 +190,9 @@ describe("ModuleManager", async () => {
|
||||
...configs.server,
|
||||
admin: {
|
||||
...configs.server.admin,
|
||||
color_scheme: "dark"
|
||||
}
|
||||
}
|
||||
color_scheme: "dark",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -201,11 +201,11 @@ describe("ModuleManager", async () => {
|
||||
|
||||
const partial = {
|
||||
auth: {
|
||||
enabled: true
|
||||
}
|
||||
enabled: true,
|
||||
},
|
||||
};
|
||||
const mm = new ModuleManager(dummyConnection, {
|
||||
initial: partial
|
||||
initial: partial,
|
||||
});
|
||||
await mm.build();
|
||||
|
||||
@@ -227,9 +227,9 @@ describe("ModuleManager", async () => {
|
||||
const mm2 = new ModuleManager(c2.dummyConnection, {
|
||||
initial: {
|
||||
auth: {
|
||||
basepath: "/shouldnt/take/this"
|
||||
}
|
||||
}
|
||||
basepath: "/shouldnt/take/this",
|
||||
},
|
||||
},
|
||||
});
|
||||
await mm2.syncConfigTable();
|
||||
const payload = {
|
||||
@@ -237,15 +237,15 @@ describe("ModuleManager", async () => {
|
||||
auth: {
|
||||
...json.auth,
|
||||
enabled: true,
|
||||
basepath: "/api/auth2"
|
||||
}
|
||||
basepath: "/api/auth2",
|
||||
},
|
||||
};
|
||||
await db
|
||||
.insertInto(TABLE_NAME)
|
||||
.values({
|
||||
type: "config",
|
||||
json: JSON.stringify(payload),
|
||||
version: CURRENT_VERSION
|
||||
version: CURRENT_VERSION,
|
||||
})
|
||||
.execute();
|
||||
await mm2.build();
|
||||
@@ -256,7 +256,7 @@ describe("ModuleManager", async () => {
|
||||
|
||||
describe("revert", async () => {
|
||||
const failingModuleSchema = Type.Object({
|
||||
value: Type.Optional(Type.Number())
|
||||
value: Type.Optional(Type.Number()),
|
||||
});
|
||||
class FailingModule extends Module<typeof failingModuleSchema> {
|
||||
getSchema() {
|
||||
@@ -301,8 +301,8 @@ describe("ModuleManager", async () => {
|
||||
const mm = new TestModuleManager(dummyConnection, {
|
||||
initial: {
|
||||
// @ts-ignore
|
||||
failing: { value: 2 }
|
||||
}
|
||||
failing: { value: 2 },
|
||||
},
|
||||
});
|
||||
await mm.build();
|
||||
expect(mm.configs()["failing"].value).toBe(2);
|
||||
@@ -313,8 +313,8 @@ describe("ModuleManager", async () => {
|
||||
const mm = new TestModuleManager(dummyConnection, {
|
||||
initial: {
|
||||
// @ts-ignore
|
||||
failing: { value: -1 }
|
||||
}
|
||||
failing: { value: -1 },
|
||||
},
|
||||
});
|
||||
expect(mm.build()).rejects.toThrow(/value must be positive/);
|
||||
expect(mm.configs()["failing"].value).toBe(-1);
|
||||
@@ -326,7 +326,7 @@ describe("ModuleManager", async () => {
|
||||
const mm = new TestModuleManager(dummyConnection, {
|
||||
onUpdated: async () => {
|
||||
mockOnUpdated();
|
||||
}
|
||||
},
|
||||
});
|
||||
await mm.build();
|
||||
// @ts-ignore
|
||||
@@ -342,11 +342,11 @@ describe("ModuleManager", async () => {
|
||||
const mm = new TestModuleManager(dummyConnection, {
|
||||
initial: {
|
||||
// @ts-ignore
|
||||
failing: { value: 1 }
|
||||
failing: { value: 1 },
|
||||
},
|
||||
onUpdated: async () => {
|
||||
mockOnUpdated();
|
||||
}
|
||||
},
|
||||
});
|
||||
await mm.build();
|
||||
expect(mm.configs()["failing"].value).toBe(1);
|
||||
@@ -354,7 +354,7 @@ describe("ModuleManager", async () => {
|
||||
// now safe mutate
|
||||
// @ts-ignore
|
||||
expect(mm.mutateConfigSafe("failing").set({ value: -2 })).rejects.toThrow(
|
||||
/value must be positive/
|
||||
/value must be positive/,
|
||||
);
|
||||
expect(mm.configs()["failing"].value).toBe(1);
|
||||
expect(mockOnUpdated).toHaveBeenCalled();
|
||||
|
||||
66
app/__test__/modules/migrations/migrations.spec.ts
Normal file
66
app/__test__/modules/migrations/migrations.spec.ts
Normal file
@@ -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");
|
||||
});
|
||||
});
|
||||
638
app/__test__/modules/migrations/samples/v7.json
Normal file
638
app/__test__/modules/migrations/samples/v7.json
Normal file
@@ -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": {}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildCon
|
||||
guard: new Guard(),
|
||||
flags: Module.ctx_flags,
|
||||
logger: new DebugLogger(false),
|
||||
...overrides
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ describe("json form", () => {
|
||||
["0", { type: "boolean" }, false],
|
||||
["on", { type: "boolean" }, true],
|
||||
["off", { type: "boolean" }, false],
|
||||
["null", { type: "null" }, null]
|
||||
["null", { type: "null" }, null],
|
||||
] satisfies [string, Exclude<JSONSchema, boolean>, any][];
|
||||
|
||||
for (const [input, schema, output] of examples) {
|
||||
@@ -35,7 +35,7 @@ describe("json form", () => {
|
||||
["array", "array", true],
|
||||
["object", "array", false],
|
||||
[["string", "number"], "number", true],
|
||||
["number", ["string", "number"], true]
|
||||
["number", ["string", "number"], true],
|
||||
] satisfies [IsTypeType, IsTypeType, boolean][];
|
||||
|
||||
for (const [type, schemaType, output] of examples) {
|
||||
@@ -48,7 +48,7 @@ describe("json form", () => {
|
||||
["#/nested/property/0/name", "#/nested/property/0"],
|
||||
["#/nested/property/0", "#/nested/property"],
|
||||
["#/nested/property", "#/nested"],
|
||||
["#/nested", "#"]
|
||||
["#/nested", "#"],
|
||||
];
|
||||
|
||||
for (const [input, output] of examples) {
|
||||
@@ -61,16 +61,16 @@ describe("json form", () => {
|
||||
[
|
||||
"#/description",
|
||||
{ type: "object", properties: { description: { type: "string" } } },
|
||||
false
|
||||
false,
|
||||
],
|
||||
[
|
||||
"#/description",
|
||||
{
|
||||
type: "object",
|
||||
required: ["description"],
|
||||
properties: { description: { type: "string" } }
|
||||
properties: { description: { type: "string" } },
|
||||
},
|
||||
true
|
||||
true,
|
||||
],
|
||||
[
|
||||
"#/nested/property",
|
||||
@@ -79,11 +79,11 @@ describe("json form", () => {
|
||||
properties: {
|
||||
nested: {
|
||||
type: "object",
|
||||
properties: { property: { type: "string" } }
|
||||
}
|
||||
}
|
||||
properties: { property: { type: "string" } },
|
||||
},
|
||||
false
|
||||
},
|
||||
},
|
||||
false,
|
||||
],
|
||||
[
|
||||
"#/nested/property",
|
||||
@@ -93,12 +93,12 @@ describe("json form", () => {
|
||||
nested: {
|
||||
type: "object",
|
||||
required: ["property"],
|
||||
properties: { property: { type: "string" } }
|
||||
}
|
||||
}
|
||||
properties: { property: { type: "string" } },
|
||||
},
|
||||
true
|
||||
]
|
||||
},
|
||||
},
|
||||
true,
|
||||
],
|
||||
] satisfies [string, Exclude<JSONSchema, boolean>, boolean][];
|
||||
|
||||
for (const [pointer, schema, output] of examples) {
|
||||
@@ -113,7 +113,7 @@ describe("json form", () => {
|
||||
["tags", "0", "0.tags"],
|
||||
["tags", 0, "0.tags"],
|
||||
["nested.property", "prefix", "prefix.nested.property"],
|
||||
["nested.property", "", "nested.property"]
|
||||
["nested.property", "", "nested.property"],
|
||||
] satisfies [string, any, string][];
|
||||
|
||||
for (const [path, prefix, output] of examples) {
|
||||
@@ -128,7 +128,7 @@ describe("json form", () => {
|
||||
["tags", "0", "tags.0"],
|
||||
["tags", 0, "tags.0"],
|
||||
["nested.property", "suffix", "nested.property.suffix"],
|
||||
["nested.property", "", "nested.property"]
|
||||
["nested.property", "", "nested.property"],
|
||||
] satisfies [string, any, string][];
|
||||
|
||||
for (const [path, suffix, output] of examples) {
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
//import type { BkndConfig } from "./src";
|
||||
|
||||
export default {
|
||||
app: {
|
||||
connection: {
|
||||
type: "libsql",
|
||||
config: {
|
||||
//url: "http://localhost:8080"
|
||||
url: ":memory:"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,257 +0,0 @@
|
||||
import { $, type Subprocess } from "bun";
|
||||
import * as esbuild from "esbuild";
|
||||
import postcss from "esbuild-postcss";
|
||||
import { entryOutputMeta } from "./internal/esbuild.entry-output-meta.plugin";
|
||||
import { guessMimeType } from "./src/media/storage/mime-types";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const watch = args.includes("--watch");
|
||||
const minify = args.includes("--minify");
|
||||
const types = args.includes("--types");
|
||||
const sourcemap = args.includes("--sourcemap");
|
||||
|
||||
type BuildOptions = esbuild.BuildOptions & { name: string };
|
||||
|
||||
const baseOptions: Partial<Omit<esbuild.BuildOptions, "plugins">> & { plugins?: any[] } = {
|
||||
minify,
|
||||
sourcemap,
|
||||
metafile: true,
|
||||
format: "esm",
|
||||
drop: ["console", "debugger"],
|
||||
loader: {
|
||||
".svg": "dataurl"
|
||||
},
|
||||
define: {
|
||||
__isDev: "0"
|
||||
}
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
type BuildFn = (format?: "esm" | "cjs") => BuildOptions;
|
||||
|
||||
// build BE
|
||||
const builds: Record<string, BuildFn> = {
|
||||
backend: (format = "esm") => ({
|
||||
...baseOptions,
|
||||
name: `backend ${format}`,
|
||||
entryPoints: [
|
||||
"src/index.ts",
|
||||
"src/data/index.ts",
|
||||
"src/core/index.ts",
|
||||
"src/core/utils/index.ts",
|
||||
"src/ui/index.ts",
|
||||
"src/ui/main.css"
|
||||
],
|
||||
outdir: "dist",
|
||||
outExtension: { ".js": format === "esm" ? ".js" : ".cjs" },
|
||||
platform: "browser",
|
||||
splitting: false,
|
||||
bundle: true,
|
||||
plugins: [postcss()],
|
||||
//target: "es2022",
|
||||
format
|
||||
}),
|
||||
/*components: (format = "esm") => ({
|
||||
...baseOptions,
|
||||
name: `components ${format}`,
|
||||
entryPoints: ["src/ui/index.ts", "src/ui/main.css"],
|
||||
outdir: "dist/ui",
|
||||
outExtension: { ".js": format === "esm" ? ".js" : ".cjs" },
|
||||
format,
|
||||
platform: "browser",
|
||||
splitting: false,
|
||||
//target: "es2022",
|
||||
bundle: true,
|
||||
//external: ["react", "react-dom", "@tanstack/react-query-devtools"],
|
||||
plugins: [postcss()],
|
||||
loader: {
|
||||
".svg": "dataurl",
|
||||
".js": "jsx"
|
||||
}
|
||||
}),*/
|
||||
static: (format = "esm") => ({
|
||||
...baseOptions,
|
||||
name: `static ${format}`,
|
||||
entryPoints: ["src/ui/main.tsx", "src/ui/main.css"],
|
||||
entryNames: "[dir]/[name]-[hash]",
|
||||
outdir: "dist/static",
|
||||
outExtension: { ".js": format === "esm" ? ".js" : ".cjs" },
|
||||
platform: "browser",
|
||||
bundle: true,
|
||||
splitting: true,
|
||||
inject: ["src/ui/inject.js"],
|
||||
target: "es2022",
|
||||
format,
|
||||
loader: {
|
||||
".svg": "dataurl",
|
||||
".js": "jsx"
|
||||
},
|
||||
define: {
|
||||
__isDev: "0",
|
||||
"process.env.NODE_ENV": '"production"'
|
||||
},
|
||||
chunkNames: "chunks/[name]-[hash]",
|
||||
plugins: [
|
||||
postcss(),
|
||||
entryOutputMeta(async (info) => {
|
||||
const manifest: Record<string, object> = {};
|
||||
const toAsset = (output: string) => {
|
||||
const name = output.split("/").pop()!;
|
||||
return {
|
||||
name,
|
||||
path: output,
|
||||
mime: guessMimeType(name)
|
||||
};
|
||||
};
|
||||
for (const { output, meta } of info) {
|
||||
manifest[meta.entryPoint as string] = toAsset(output);
|
||||
if (meta.cssBundle) {
|
||||
manifest["src/ui/main.css"] = toAsset(meta.cssBundle);
|
||||
}
|
||||
}
|
||||
|
||||
const manifest_file = "dist/static/manifest.json";
|
||||
await Bun.write(manifest_file, JSON.stringify(manifest, null, 2));
|
||||
console.log(`Manifest written to ${manifest_file}`, manifest);
|
||||
})
|
||||
]
|
||||
})
|
||||
};
|
||||
|
||||
function adapter(adapter: string, overrides: Partial<esbuild.BuildOptions> = {}): BuildOptions {
|
||||
return {
|
||||
...baseOptions,
|
||||
name: `adapter ${adapter} ${overrides?.format === "cjs" ? "cjs" : "esm"}`,
|
||||
entryPoints: [`src/adapter/${adapter}`],
|
||||
platform: "neutral",
|
||||
outfile: `dist/adapter/${adapter}/index.${overrides?.format === "cjs" ? "cjs" : "js"}`,
|
||||
external: [
|
||||
"cloudflare:workers",
|
||||
"@hono*",
|
||||
"hono*",
|
||||
"bknd*",
|
||||
"*.html",
|
||||
"node*",
|
||||
"react*",
|
||||
"next*",
|
||||
"libsql",
|
||||
"@libsql*"
|
||||
],
|
||||
splitting: false,
|
||||
treeShaking: true,
|
||||
bundle: true,
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
const adapters = [
|
||||
adapter("vite", { platform: "node" }),
|
||||
adapter("cloudflare"),
|
||||
adapter("nextjs", { platform: "node", format: "esm" }),
|
||||
adapter("nextjs", { platform: "node", format: "cjs" }),
|
||||
adapter("remix", { format: "esm" }),
|
||||
adapter("remix", { format: "cjs" }),
|
||||
adapter("bun"),
|
||||
adapter("node", { platform: "node", format: "esm" }),
|
||||
adapter("node", { platform: "node", format: "cjs" })
|
||||
];
|
||||
|
||||
const collect = [
|
||||
builds.static(),
|
||||
builds.backend(),
|
||||
//builds.components(),
|
||||
builds.backend("cjs"),
|
||||
//builds.components("cjs"),
|
||||
...adapters
|
||||
];
|
||||
|
||||
if (watch) {
|
||||
const _state: {
|
||||
timeout: Timer | undefined;
|
||||
cleanup: Subprocess | undefined;
|
||||
building: Subprocess | undefined;
|
||||
} = {
|
||||
timeout: undefined,
|
||||
cleanup: undefined,
|
||||
building: undefined
|
||||
};
|
||||
|
||||
async function rebuildTypes() {
|
||||
if (!types) return;
|
||||
if (_state.timeout) {
|
||||
clearTimeout(_state.timeout);
|
||||
if (_state.cleanup) _state.cleanup.kill();
|
||||
if (_state.building) _state.building.kill();
|
||||
}
|
||||
_state.timeout = setTimeout(async () => {
|
||||
_state.cleanup = Bun.spawn(["bun", "clean:types"], {
|
||||
onExit: () => {
|
||||
_state.cleanup = undefined;
|
||||
_state.building = Bun.spawn(["bun", "build:types"], {
|
||||
onExit: () => {
|
||||
_state.building = undefined;
|
||||
console.log("Types rebuilt");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
for (const { name, ...build } of collect) {
|
||||
const ctx = await esbuild.context({
|
||||
...build,
|
||||
plugins: [
|
||||
...(build.plugins ?? []),
|
||||
{
|
||||
name: "rebuild-notify",
|
||||
setup(build) {
|
||||
build.onEnd((result) => {
|
||||
console.log(`rebuilt ${name} with ${result.errors.length} errors`);
|
||||
rebuildTypes();
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
ctx.watch();
|
||||
}
|
||||
} else {
|
||||
await $`rm -rf dist`;
|
||||
|
||||
async function _build() {
|
||||
let i = 0;
|
||||
const count = collect.length;
|
||||
for await (const { name, ...build } of collect) {
|
||||
await esbuild.build({
|
||||
...build,
|
||||
plugins: [
|
||||
...(build.plugins || []),
|
||||
{
|
||||
name: "progress",
|
||||
setup(build) {
|
||||
i++;
|
||||
build.onEnd((result) => {
|
||||
const errors = result.errors.length;
|
||||
const from = String(i).padStart(String(count).length);
|
||||
console.log(`[${from}/${count}] built ${name} with ${errors} errors`);
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
console.log("All builds complete");
|
||||
}
|
||||
|
||||
async function _buildtypes() {
|
||||
if (!types) return;
|
||||
Bun.spawn(["bun", "build:types"], {
|
||||
onExit: () => {
|
||||
console.log("Types rebuilt");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await Promise.all([_build(), _buildtypes()]);
|
||||
}
|
||||
87
app/build.ts
87
app/build.ts
@@ -27,9 +27,9 @@ function buildTypes() {
|
||||
onExit: () => {
|
||||
console.log("Types aliased");
|
||||
types_running = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -46,41 +46,52 @@ if (types && !watch) {
|
||||
buildTypes();
|
||||
}
|
||||
|
||||
function banner(title: string) {
|
||||
console.log("");
|
||||
console.log("=".repeat(40));
|
||||
console.log(title.toUpperCase());
|
||||
console.log("-".repeat(40));
|
||||
}
|
||||
|
||||
/**
|
||||
* Building backend and general API
|
||||
*/
|
||||
async function buildApi() {
|
||||
banner("Building API");
|
||||
await tsup.build({
|
||||
minify,
|
||||
sourcemap,
|
||||
watch,
|
||||
entry: ["src/index.ts", "src/data/index.ts", "src/core/index.ts", "src/core/utils/index.ts"],
|
||||
outDir: "dist",
|
||||
external: ["bun:test", "@libsql/client", "bknd/client"],
|
||||
external: ["bun:test", "@libsql/client"],
|
||||
metafile: true,
|
||||
platform: "browser",
|
||||
format: ["esm"],
|
||||
splitting: false,
|
||||
treeshake: true,
|
||||
loader: {
|
||||
".svg": "dataurl"
|
||||
".svg": "dataurl",
|
||||
},
|
||||
onSuccess: async () => {
|
||||
delayTypes();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function rewriteClient(path: string) {
|
||||
const bundle = await Bun.file(path).text();
|
||||
await Bun.write(path, '"use client";\n' + bundle.replaceAll("ui/client", "bknd/client"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Building UI for direct imports
|
||||
*/
|
||||
async function buildUi() {
|
||||
await tsup.build({
|
||||
const base = {
|
||||
minify,
|
||||
sourcemap,
|
||||
watch,
|
||||
entry: ["src/ui/index.ts", "src/ui/client/index.ts", "src/ui/main.css", "src/ui/styles.css"],
|
||||
outDir: "dist/ui",
|
||||
external: [
|
||||
"bun:test",
|
||||
"react",
|
||||
@@ -90,7 +101,7 @@ async function buildUi() {
|
||||
"use-sync-external-store",
|
||||
/codemirror/,
|
||||
"@xyflow/react",
|
||||
"@mantine/core"
|
||||
"@mantine/core",
|
||||
],
|
||||
metafile: true,
|
||||
platform: "browser",
|
||||
@@ -99,14 +110,33 @@ async function buildUi() {
|
||||
bundle: true,
|
||||
treeshake: true,
|
||||
loader: {
|
||||
".svg": "dataurl"
|
||||
".svg": "dataurl",
|
||||
},
|
||||
esbuildOptions: (options) => {
|
||||
options.logLevel = "silent";
|
||||
},
|
||||
} satisfies tsup.Options;
|
||||
|
||||
banner("Building UI");
|
||||
await tsup.build({
|
||||
...base,
|
||||
entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"],
|
||||
outDir: "dist/ui",
|
||||
onSuccess: async () => {
|
||||
await rewriteClient("./dist/ui/index.js");
|
||||
delayTypes();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
banner("Building Client");
|
||||
await tsup.build({
|
||||
...base,
|
||||
entry: ["src/ui/client/index.ts"],
|
||||
outDir: "dist/ui/client",
|
||||
onSuccess: async () => {
|
||||
await rewriteClient("./dist/ui/client/index.js");
|
||||
delayTypes();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -116,6 +146,7 @@ async function buildUi() {
|
||||
* - ui/client is external, and after built replaced with "bknd/client"
|
||||
*/
|
||||
async function buildUiElements() {
|
||||
banner("Building UI Elements");
|
||||
await tsup.build({
|
||||
minify,
|
||||
sourcemap,
|
||||
@@ -128,7 +159,7 @@ async function buildUiElements() {
|
||||
"react-dom",
|
||||
"react/jsx-runtime",
|
||||
"react/jsx-dev-runtime",
|
||||
"use-sync-external-store"
|
||||
"use-sync-external-store",
|
||||
],
|
||||
metafile: true,
|
||||
platform: "browser",
|
||||
@@ -137,22 +168,18 @@ async function buildUiElements() {
|
||||
bundle: true,
|
||||
treeshake: true,
|
||||
loader: {
|
||||
".svg": "dataurl"
|
||||
".svg": "dataurl",
|
||||
},
|
||||
esbuildOptions: (options) => {
|
||||
options.alias = {
|
||||
// not important for elements, mock to reduce bundle
|
||||
"tailwind-merge": "./src/ui/elements/mocks/tailwind-merge.ts"
|
||||
"tailwind-merge": "./src/ui/elements/mocks/tailwind-merge.ts",
|
||||
};
|
||||
},
|
||||
onSuccess: async () => {
|
||||
// manually replace ui/client with bknd/client
|
||||
const path = "./dist/ui/elements/index.js";
|
||||
const bundle = await Bun.file(path).text();
|
||||
await Bun.write(path, bundle.replaceAll("ui/client", "bknd/client"));
|
||||
|
||||
await rewriteClient("./dist/ui/elements/index.js");
|
||||
delayTypes();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -176,49 +203,51 @@ function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsu
|
||||
...overrides,
|
||||
define: {
|
||||
__isDev: "0",
|
||||
...overrides.define
|
||||
...overrides.define,
|
||||
},
|
||||
external: [
|
||||
/^cloudflare*/,
|
||||
/^@?(hono|libsql).*?/,
|
||||
/^(bknd|react|next|node).*?/,
|
||||
/.*\.(html)$/,
|
||||
...(Array.isArray(overrides.external) ? overrides.external : [])
|
||||
]
|
||||
...(Array.isArray(overrides.external) ? overrides.external : []),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async function buildAdapters() {
|
||||
banner("Building Adapters");
|
||||
// base adapter handles
|
||||
await tsup.build({
|
||||
...baseConfig(""),
|
||||
entry: ["src/adapter/index.ts"],
|
||||
outDir: "dist/adapter"
|
||||
outDir: "dist/adapter",
|
||||
});
|
||||
|
||||
// specific adatpers
|
||||
await tsup.build(baseConfig("remix"));
|
||||
await tsup.build(baseConfig("bun"));
|
||||
await tsup.build(baseConfig("astro"));
|
||||
await tsup.build(baseConfig("aws"));
|
||||
await tsup.build(
|
||||
baseConfig("cloudflare", {
|
||||
external: [/^kysely/]
|
||||
})
|
||||
external: [/^kysely/],
|
||||
}),
|
||||
);
|
||||
|
||||
await tsup.build({
|
||||
...baseConfig("vite"),
|
||||
platform: "node"
|
||||
platform: "node",
|
||||
});
|
||||
|
||||
await tsup.build({
|
||||
...baseConfig("nextjs"),
|
||||
platform: "node"
|
||||
platform: "node",
|
||||
});
|
||||
|
||||
await tsup.build({
|
||||
...baseConfig("node"),
|
||||
platform: "node"
|
||||
platform: "node",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ export const entryOutputMeta = (
|
||||
outputs: {
|
||||
output: string;
|
||||
meta: Metafile["outputs"][string];
|
||||
}[]
|
||||
) => void | Promise<void>
|
||||
}[],
|
||||
) => void | Promise<void>,
|
||||
): Plugin => ({
|
||||
name: "report-entry-output-plugin",
|
||||
setup(build) {
|
||||
@@ -29,5 +29,5 @@ export const entryOutputMeta = (
|
||||
await onComplete?.(outputs);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"bin": "./dist/cli/index.js",
|
||||
"version": "0.8.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.",
|
||||
"homepage": "https://bknd.io",
|
||||
"repository": {
|
||||
@@ -32,79 +32,83 @@
|
||||
},
|
||||
"license": "FSL-1.1-MIT",
|
||||
"dependencies": {
|
||||
"@cfworker/json-schema": "^2.0.1",
|
||||
"@cfworker/json-schema": "^4.1.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",
|
||||
"@codemirror/lang-liquid": "^6.2.2",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@libsql/client": "^0.14.0",
|
||||
"@mantine/core": "^7.13.4",
|
||||
"@sinclair/typebox": "^0.32.34",
|
||||
"@tanstack/react-form": "0.19.2",
|
||||
"@uiw/react-codemirror": "^4.23.6",
|
||||
"@xyflow/react": "^12.3.2",
|
||||
"aws4fetch": "^1.0.18",
|
||||
"@mantine/core": "^7.17.1",
|
||||
"@mantine/hooks": "^7.17.1",
|
||||
"@sinclair/typebox": "^0.34.30",
|
||||
"@tanstack/react-form": "^1.0.5",
|
||||
"@uiw/react-codemirror": "^4.23.10",
|
||||
"@xyflow/react": "^12.4.4",
|
||||
"aws4fetch": "^1.0.20",
|
||||
"dayjs": "^1.11.13",
|
||||
"fast-xml-parser": "^4.4.0",
|
||||
"hono": "^4.6.12",
|
||||
"fast-xml-parser": "^5.0.8",
|
||||
"hono": "^4.7.4",
|
||||
"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",
|
||||
"liquidjs": "^10.15.0",
|
||||
"kysely": "^0.27.6",
|
||||
"liquidjs": "^10.21.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"oauth4webapi": "^2.11.1",
|
||||
"object-path-immutable": "^4.1.2",
|
||||
"picocolors": "^1.1.1",
|
||||
"radix-ui": "^1.1.2",
|
||||
"swr": "^2.2.5"
|
||||
"radix-ui": "^1.1.3",
|
||||
"swr": "^2.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aws-sdk/client-s3": "^3.613.0",
|
||||
"@aws-sdk/client-s3": "^3.758.0",
|
||||
"@bluwy/giget-core": "^0.1.2",
|
||||
"@dagrejs/dagre": "^1.1.4",
|
||||
"@hono/typebox-validator": "^0.2.6",
|
||||
"@hono/vite-dev-server": "^0.17.0",
|
||||
"@hono/zod-validator": "^0.4.1",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@hono/typebox-validator": "^0.3.2",
|
||||
"@hono/vite-dev-server": "^0.19.0",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@libsql/kysely-libsql": "^0.4.1",
|
||||
"@rjsf/core": "^5.22.2",
|
||||
"@mantine/modals": "^7.17.1",
|
||||
"@mantine/notifications": "^7.17.1",
|
||||
"@rjsf/core": "5.22.2",
|
||||
"@tabler/icons-react": "3.18.0",
|
||||
"@types/node": "^22.10.0",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"@tailwindcss/postcss": "^4.0.12",
|
||||
"@tailwindcss/vite": "^4.0.12",
|
||||
"@types/node": "^22.13.10",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"clsx": "^2.1.1",
|
||||
"dotenv": "^16.4.7",
|
||||
"esbuild-postcss": "^0.0.4",
|
||||
"jotai": "^2.10.1",
|
||||
"jotai": "^2.12.2",
|
||||
"kysely-d1": "^0.3.0",
|
||||
"open": "^10.1.0",
|
||||
"openapi-types": "^12.1.3",
|
||||
"postcss": "^8.4.47",
|
||||
"postcss": "^8.5.3",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"react-hook-form": "^7.53.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-icons": "5.2.1",
|
||||
"react-json-view-lite": "^2.0.1",
|
||||
"sql-formatter": "^15.4.9",
|
||||
"tailwind-merge": "^2.5.4",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"react-json-view-lite": "^2.4.1",
|
||||
"sql-formatter": "^15.4.11",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss": "^4.0.12",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tsc-alias": "^1.8.10",
|
||||
"tsup": "^8.3.5",
|
||||
"vite": "^5.4.10",
|
||||
"vite-plugin-static-copy": "^2.0.0",
|
||||
"vite-tsconfig-paths": "^5.0.1",
|
||||
"wouter": "^3.3.5"
|
||||
"tsc-alias": "^1.8.11",
|
||||
"tsup": "^8.4.0",
|
||||
"vite": "^6.2.1",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"wouter": "^3.6.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@hono/node-server": "^1.13.7"
|
||||
"@hono/node-server": "^1.13.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
"react": "^19.x",
|
||||
"react-dom": "^19.x"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
@@ -189,6 +193,11 @@
|
||||
"import": "./dist/adapter/astro/index.js",
|
||||
"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/styles.css": "./dist/ui/styles.css",
|
||||
"./dist/manifest.json": "./dist/static/.vite/manifest.json"
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"postcss-import": {},
|
||||
"tailwindcss/nesting": {},
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
"@tailwindcss/postcss": {},
|
||||
"postcss-preset-mantine": {},
|
||||
"postcss-simple-vars": {
|
||||
variables: {
|
||||
@@ -11,8 +8,8 @@ export default {
|
||||
"mantine-breakpoint-sm": "48em",
|
||||
"mantine-breakpoint-md": "62em",
|
||||
"mantine-breakpoint-lg": "75em",
|
||||
"mantine-breakpoint-xl": "88em"
|
||||
}
|
||||
}
|
||||
}
|
||||
"mantine-breakpoint-xl": "88em",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,9 +2,9 @@ import type { SafeUser } from "auth";
|
||||
import { AuthApi } from "auth/api/AuthApi";
|
||||
import { DataApi } from "data/api/DataApi";
|
||||
import { decode } from "hono/jwt";
|
||||
import { omit } from "lodash-es";
|
||||
import { MediaApi } from "media/api/MediaApi";
|
||||
import { SystemApi } from "modules/SystemApi";
|
||||
import { omitKeys } from "core/utils";
|
||||
|
||||
export type TApiUser = SafeUser;
|
||||
|
||||
@@ -122,7 +122,7 @@ export class Api {
|
||||
this.verified = false;
|
||||
|
||||
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 {
|
||||
this.user = undefined;
|
||||
}
|
||||
@@ -153,7 +153,7 @@ export class Api {
|
||||
return {
|
||||
token: this.token,
|
||||
user: this.user,
|
||||
verified: this.verified
|
||||
verified: this.verified,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ export class Api {
|
||||
token: this.token,
|
||||
headers: this.options.headers,
|
||||
token_transport: this.token_transport,
|
||||
verbose: this.options.verbose
|
||||
verbose: this.options.verbose,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -211,9 +211,9 @@ export class Api {
|
||||
this.auth = new AuthApi(
|
||||
{
|
||||
...baseParams,
|
||||
onTokenUpdate: (token) => this.updateToken(token, true)
|
||||
onTokenUpdate: (token) => this.updateToken(token, true),
|
||||
},
|
||||
fetcher
|
||||
fetcher,
|
||||
);
|
||||
this.media = new MediaApi(baseParams, fetcher);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Api, type ApiOptions } from "Api";
|
||||
import type { CreateUserPayload } from "auth/AppAuth";
|
||||
import { $console } from "core";
|
||||
import { Event } from "core/events";
|
||||
@@ -9,12 +8,15 @@ import {
|
||||
type ModuleBuildContext,
|
||||
ModuleManager,
|
||||
type ModuleManagerOptions,
|
||||
type Modules
|
||||
type Modules,
|
||||
} from "modules/ModuleManager";
|
||||
import * as SystemPermissions from "modules/permissions";
|
||||
import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
|
||||
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;
|
||||
|
||||
abstract class AppEvent<A = {}> extends Event<{ app: App } & A> {}
|
||||
@@ -31,7 +33,7 @@ export const AppEvents = { AppConfigUpdatedEvent, AppBuiltEvent, AppFirstBoot }
|
||||
|
||||
export type AppOptions = {
|
||||
plugins?: AppPlugin[];
|
||||
seed?: (ctx: ModuleBuildContext) => Promise<void>;
|
||||
seed?: (ctx: ModuleBuildContext & { app: App }) => Promise<void>;
|
||||
manager?: Omit<ModuleManagerOptions, "initial" | "onUpdated" | "seed">;
|
||||
};
|
||||
export type CreateAppConfig = {
|
||||
@@ -48,6 +50,7 @@ export type CreateAppConfig = {
|
||||
};
|
||||
|
||||
export type AppConfig = InitialModuleConfigs;
|
||||
export type LocalApiOptions = Request | ApiOptions;
|
||||
|
||||
export class App {
|
||||
modules: ModuleManager;
|
||||
@@ -55,17 +58,18 @@ export class App {
|
||||
adminController?: AdminController;
|
||||
private trigger_first_boot = false;
|
||||
private plugins: AppPlugin[];
|
||||
private _id: string = crypto.randomUUID();
|
||||
private _building: boolean = false;
|
||||
|
||||
constructor(
|
||||
private connection: Connection,
|
||||
_initialConfig?: InitialModuleConfigs,
|
||||
private options?: AppOptions
|
||||
private options?: AppOptions,
|
||||
) {
|
||||
this.plugins = options?.plugins ?? [];
|
||||
this.modules = new ModuleManager(connection, {
|
||||
...(options?.manager ?? {}),
|
||||
initial: _initialConfig,
|
||||
seed: options?.seed,
|
||||
onUpdated: async (key, config) => {
|
||||
// if the EventManager was disabled, we assume we shouldn't
|
||||
// respond to events, such as "onUpdated".
|
||||
@@ -88,8 +92,13 @@ export class App {
|
||||
server.use(async (c, next) => {
|
||||
c.set("app", this);
|
||||
await next();
|
||||
|
||||
try {
|
||||
// gracefully add the app id
|
||||
c.res.headers.set("X-bknd-id", this._id);
|
||||
} catch (e) {}
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
this.modules.ctx().emgr.registerEvents(AppEvents);
|
||||
}
|
||||
@@ -98,9 +107,18 @@ export class App {
|
||||
return this.modules.ctx().emgr;
|
||||
}
|
||||
|
||||
async build(options?: { sync?: boolean }) {
|
||||
async build(options?: { sync?: boolean; fetch?: boolean; forceBuild?: boolean }) {
|
||||
// prevent multiple concurrent builds
|
||||
if (this._building) {
|
||||
while (this._building) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
if (!options?.forceBuild) return;
|
||||
}
|
||||
this._building = true;
|
||||
|
||||
if (options?.sync) this.modules.ctx().flags.sync_required = true;
|
||||
await this.modules.build();
|
||||
await this.modules.build({ fetch: options?.fetch });
|
||||
|
||||
const { guard, server } = this.modules.ctx();
|
||||
|
||||
@@ -113,13 +131,20 @@ export class App {
|
||||
await Promise.all(this.plugins.map((plugin) => plugin(this)));
|
||||
}
|
||||
|
||||
$console.log("App built");
|
||||
await this.emgr.emit(new AppBuiltEvent({ app: this }));
|
||||
|
||||
// first boot is set from ModuleManager when there wasn't a config table
|
||||
if (this.trigger_first_boot) {
|
||||
this.trigger_first_boot = false;
|
||||
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) {
|
||||
@@ -144,8 +169,8 @@ export class App {
|
||||
{
|
||||
get: (_, module: keyof Modules) => {
|
||||
return this.modules.get(module);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
) as Modules;
|
||||
}
|
||||
|
||||
@@ -180,13 +205,13 @@ export class App {
|
||||
return this.module.auth.createUser(p);
|
||||
}
|
||||
|
||||
getApi(options: Request | ApiOptions = {}) {
|
||||
getApi(options?: LocalApiOptions) {
|
||||
const fetcher = this.server.request as typeof fetch;
|
||||
if (options instanceof Request) {
|
||||
if (options && options instanceof Request) {
|
||||
return new Api({ request: options, headers: options.headers, fetcher });
|
||||
}
|
||||
|
||||
return new Api({ host: "http://localhost", ...options, fetcher });
|
||||
return new Api({ host: "http://localhost", ...(options ?? {}), fetcher });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,7 +224,7 @@ export function createApp(config: CreateAppConfig = {}) {
|
||||
} else if (typeof config.connection === "object") {
|
||||
if ("type" in config.connection) {
|
||||
$console.warn(
|
||||
"Using deprecated connection type 'libsql', use the 'config' object directly."
|
||||
"Using deprecated connection type 'libsql', use the 'config' object directly.",
|
||||
);
|
||||
connection = new LibsqlConnection(config.connection.config);
|
||||
} else {
|
||||
|
||||
@@ -17,7 +17,7 @@ export type Options = {
|
||||
export async function getApi(Astro: TAstro, options: Options = { mode: "static" }) {
|
||||
const api = new Api({
|
||||
host: new URL(Astro.request.url).origin,
|
||||
headers: options.mode === "dynamic" ? Astro.request.headers : undefined
|
||||
headers: options.mode === "dynamic" ? Astro.request.headers : undefined,
|
||||
});
|
||||
await api.verifyAuth();
|
||||
return api;
|
||||
|
||||
68
app/src/adapter/aws/aws-lambda.adapter.ts
Normal file
68
app/src/adapter/aws/aws-lambda.adapter.ts
Normal file
@@ -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);
|
||||
};
|
||||
}
|
||||
1
app/src/adapter/aws/index.ts
Normal file
1
app/src/adapter/aws/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./aws-lambda.adapter";
|
||||
@@ -19,7 +19,7 @@ export async function createApp({ distPath, ...config }: RuntimeBkndConfig = {})
|
||||
registerLocalMediaAdapter();
|
||||
app = await createRuntimeApp({
|
||||
...config,
|
||||
serveStatic: serveStatic({ root })
|
||||
serveStatic: serveStatic({ root }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ export function serve({
|
||||
port = config.server.default_port,
|
||||
onBuilt,
|
||||
buildConfig,
|
||||
adminOptions,
|
||||
...serveOptions
|
||||
}: BunBkndConfig = {}) {
|
||||
Bun.serve({
|
||||
@@ -46,10 +47,11 @@ export function serve({
|
||||
options,
|
||||
onBuilt,
|
||||
buildConfig,
|
||||
distPath
|
||||
adminOptions,
|
||||
distPath,
|
||||
});
|
||||
return app.fetch(request);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Server is running on http://localhost:${port}`);
|
||||
|
||||
@@ -12,7 +12,7 @@ export type D1ConnectionConfig = {
|
||||
class CustomD1Dialect extends D1Dialect {
|
||||
override createIntrospector(db: Kysely<any>): DatabaseIntrospector {
|
||||
return new SqliteIntrospector(db, {
|
||||
excludeTables: ["_cf_KV"]
|
||||
excludeTables: ["_cf_KV"],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ export class D1Connection extends SqliteConnection {
|
||||
|
||||
const kysely = new Kysely({
|
||||
dialect: new CustomD1Dialect({ database: config.binding }),
|
||||
plugins
|
||||
plugins,
|
||||
});
|
||||
super(kysely, {}, plugins);
|
||||
}
|
||||
@@ -37,7 +37,7 @@ export class D1Connection extends SqliteConnection {
|
||||
}
|
||||
|
||||
protected override async batch<Queries extends QB[]>(
|
||||
queries: [...Queries]
|
||||
queries: [...Queries],
|
||||
): Promise<{
|
||||
[K in keyof Queries]: Awaited<ReturnType<Queries[K]["execute"]>>;
|
||||
}> {
|
||||
@@ -47,7 +47,7 @@ export class D1Connection extends SqliteConnection {
|
||||
queries.map((q) => {
|
||||
const { sql, parameters } = q.compile();
|
||||
return db.prepare(sql).bind(...parameters);
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// let it run through plugins
|
||||
|
||||
@@ -8,9 +8,9 @@ import { getBindings } from "./bindings";
|
||||
export function makeSchema(bindings: string[] = []) {
|
||||
return Type.Object(
|
||||
{
|
||||
binding: bindings.length > 0 ? StringEnum(bindings) : Type.Optional(Type.String())
|
||||
binding: bindings.length > 0 ? StringEnum(bindings) : Type.Optional(Type.String()),
|
||||
},
|
||||
{ title: "R2", description: "Cloudflare R2 storage" }
|
||||
{ title: "R2", description: "Cloudflare R2 storage" },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,10 +36,10 @@ export function registerMedia(env: Record<string, any>) {
|
||||
override toJSON() {
|
||||
return {
|
||||
...super.toJSON(),
|
||||
config: this.config
|
||||
config: this.config,
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -67,13 +67,13 @@ export class StorageR2Adapter implements StorageAdapter {
|
||||
}
|
||||
}
|
||||
async listObjects(
|
||||
prefix?: string
|
||||
prefix?: string,
|
||||
): Promise<{ key: string; last_modified: Date; size: number }[]> {
|
||||
const list = await this.bucket.list({ limit: 50 });
|
||||
return list.objects.map((item) => ({
|
||||
key: item.key,
|
||||
size: item.size,
|
||||
last_modified: item.uploaded
|
||||
last_modified: item.uploaded,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ export class StorageR2Adapter implements StorageAdapter {
|
||||
let object: R2ObjectBody | null;
|
||||
const responseHeaders = new Headers({
|
||||
"Accept-Ranges": "bytes",
|
||||
"Content-Type": guess(key)
|
||||
"Content-Type": guess(key),
|
||||
});
|
||||
|
||||
//console.log("getObject:headers", headersToObject(headers));
|
||||
@@ -98,7 +98,7 @@ export class StorageR2Adapter implements StorageAdapter {
|
||||
? {} // miniflare doesn't support range requests
|
||||
: {
|
||||
range: headers,
|
||||
onlyIf: headers
|
||||
onlyIf: headers,
|
||||
};
|
||||
object = (await this.bucket.get(key, options)) as R2ObjectBody;
|
||||
|
||||
@@ -130,7 +130,7 @@ export class StorageR2Adapter implements StorageAdapter {
|
||||
|
||||
return new Response(object.body, {
|
||||
status: object.range ? 206 : 200,
|
||||
headers: responseHeaders
|
||||
headers: responseHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ export class StorageR2Adapter implements StorageAdapter {
|
||||
if (!metadata || Object.keys(metadata).length === 0) {
|
||||
// guessing is especially required for dev environment (miniflare)
|
||||
metadata = {
|
||||
contentType: guess(object.key)
|
||||
contentType: guess(object.key),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ export class StorageR2Adapter implements StorageAdapter {
|
||||
|
||||
return {
|
||||
type: String(head.httpMetadata?.contentType ?? guess(key)),
|
||||
size: head.size
|
||||
size: head.size,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ export class StorageR2Adapter implements StorageAdapter {
|
||||
toJSON(secrets?: boolean) {
|
||||
return {
|
||||
type: this.getName(),
|
||||
config: {}
|
||||
config: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export function getBindings<T extends GetBindingType>(env: any, type: T): Bindin
|
||||
if (env[key] && (env[key] as any).constructor.name === type) {
|
||||
bindings.push({
|
||||
key,
|
||||
value: env[key] as BindingTypeMap[T]
|
||||
value: env[key] as BindingTypeMap[T],
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
@@ -84,7 +84,7 @@ export function serve<Env = any>(config: CloudflareBkndConfig<Env> = {}) {
|
||||
hono.all("*", async (c, next) => {
|
||||
const res = await serveStatic({
|
||||
path: `./${pathname}`,
|
||||
manifest: config.manifest!
|
||||
manifest: config.manifest!,
|
||||
})(c as any, next);
|
||||
if (res instanceof Response) {
|
||||
const ttl = 60 * 60 * 24 * 365;
|
||||
@@ -114,6 +114,6 @@ export function serve<Env = any>(config: CloudflareBkndConfig<Env> = {}) {
|
||||
default:
|
||||
throw new Error(`Unknown mode ${mode}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export {
|
||||
getBindings,
|
||||
type BindingTypeMap,
|
||||
type GetBindingType,
|
||||
type BindingMap
|
||||
type BindingMap,
|
||||
} from "./bindings";
|
||||
|
||||
export function d1(config: D1ConnectionConfig) {
|
||||
|
||||
@@ -31,13 +31,13 @@ export async function getCached(config: CloudflareBkndConfig, { env, ctx, ...arg
|
||||
async ({ params: { app } }) => {
|
||||
saveConfig(app.toJSON(true));
|
||||
},
|
||||
"sync"
|
||||
"sync",
|
||||
);
|
||||
await config.beforeBuild?.(app);
|
||||
},
|
||||
adminOptions: { html: config.html }
|
||||
adminOptions: { html: config.html },
|
||||
},
|
||||
{ env, ctx, ...args }
|
||||
{ env, ctx, ...args },
|
||||
);
|
||||
|
||||
if (!cachedConfig) {
|
||||
|
||||
@@ -23,7 +23,7 @@ export async function getDurable(config: CloudflareBkndConfig, ctx: Context) {
|
||||
config: create_config,
|
||||
html: config.html,
|
||||
keepAliveSeconds: config.keepAliveSeconds,
|
||||
setAdminHtml: config.setAdminHtml
|
||||
setAdminHtml: config.setAdminHtml,
|
||||
});
|
||||
|
||||
const headers = new Headers(res.headers);
|
||||
@@ -32,7 +32,7 @@ export async function getDurable(config: CloudflareBkndConfig, ctx: Context) {
|
||||
return new Response(res.body, {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
headers
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export class DurableBkndApp extends DurableObject {
|
||||
html?: string;
|
||||
keepAliveSeconds?: number;
|
||||
setAdminHtml?: boolean;
|
||||
}
|
||||
},
|
||||
) {
|
||||
let buildtime = 0;
|
||||
if (!this.app) {
|
||||
@@ -73,7 +73,7 @@ export class DurableBkndApp extends DurableObject {
|
||||
return c.json({
|
||||
id: this.id,
|
||||
keepAliveSeconds: options?.keepAliveSeconds ?? 0,
|
||||
colo: context.colo
|
||||
colo: context.colo,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -82,7 +82,7 @@ export class DurableBkndApp extends DurableObject {
|
||||
adminOptions: { html: options.html },
|
||||
beforeBuild: async (app) => {
|
||||
await this.beforeBuild(app);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
buildtime = performance.now() - start;
|
||||
@@ -101,7 +101,7 @@ export class DurableBkndApp extends DurableObject {
|
||||
return new Response(res.body, {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
headers
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ export async function makeApp(config: CloudflareBkndConfig, ctx: Context) {
|
||||
return await createRuntimeApp(
|
||||
{
|
||||
...makeCfConfig(config, ctx),
|
||||
adminOptions: config.html ? { html: config.html } : undefined
|
||||
adminOptions: config.html ? { html: config.html } : undefined,
|
||||
},
|
||||
ctx
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ export type FrameworkBkndConfig<Args = any> = BkndConfig<Args>;
|
||||
|
||||
export type RuntimeBkndConfig<Args = any> = BkndConfig<Args> & {
|
||||
distPath?: string;
|
||||
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
|
||||
adminOptions?: AdminControllerOptions | false;
|
||||
};
|
||||
|
||||
export function makeConfig<Args = any>(config: BkndConfig<Args>, args?: Args): CreateAppConfig {
|
||||
@@ -34,7 +36,7 @@ export function makeConfig<Args = any>(config: BkndConfig<Args>, args?: Args): C
|
||||
|
||||
export async function createFrameworkApp<Args = any>(
|
||||
config: FrameworkBkndConfig,
|
||||
args?: Args
|
||||
args?: Args,
|
||||
): Promise<App> {
|
||||
const app = App.create(makeConfig(config, args));
|
||||
|
||||
@@ -44,7 +46,7 @@ export async function createFrameworkApp<Args = any>(
|
||||
async () => {
|
||||
await config.onBuilt?.(app);
|
||||
},
|
||||
"sync"
|
||||
"sync",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,15 +57,8 @@ export async function createFrameworkApp<Args = any>(
|
||||
}
|
||||
|
||||
export async function createRuntimeApp<Env = any>(
|
||||
{
|
||||
serveStatic,
|
||||
adminOptions,
|
||||
...config
|
||||
}: RuntimeBkndConfig & {
|
||||
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
|
||||
adminOptions?: AdminControllerOptions | false;
|
||||
},
|
||||
env?: Env
|
||||
{ serveStatic, adminOptions, ...config }: RuntimeBkndConfig,
|
||||
env?: Env,
|
||||
): Promise<App> {
|
||||
const app = App.create(makeConfig(config, env));
|
||||
|
||||
@@ -82,7 +77,7 @@ export async function createRuntimeApp<Env = any>(
|
||||
app.registerAdminController(adminOptions);
|
||||
}
|
||||
},
|
||||
"sync"
|
||||
"sync",
|
||||
);
|
||||
|
||||
await config.beforeBuild?.(app);
|
||||
|
||||
@@ -1,64 +1,60 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { nodeRequestToRequest } from "adapter/utils";
|
||||
import type { App } from "bknd";
|
||||
import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter";
|
||||
import { Api } from "bknd/client";
|
||||
import { getRuntimeKey, isNode } from "core/utils";
|
||||
|
||||
export type NextjsBkndConfig = FrameworkBkndConfig & {
|
||||
cleanSearch?: string[];
|
||||
cleanRequest?: { searchParams?: string[] };
|
||||
};
|
||||
|
||||
type GetServerSidePropsContext = {
|
||||
req: IncomingMessage;
|
||||
res: ServerResponse;
|
||||
params?: Params;
|
||||
query: any;
|
||||
preview?: boolean;
|
||||
previewData?: any;
|
||||
draftMode?: boolean;
|
||||
resolvedUrl: string;
|
||||
locale?: string;
|
||||
locales?: string[];
|
||||
defaultLocale?: string;
|
||||
};
|
||||
let app: App;
|
||||
let building: boolean = false;
|
||||
|
||||
export function createApi({ req }: GetServerSidePropsContext) {
|
||||
const request = nodeRequestToRequest(req);
|
||||
return new Api({
|
||||
host: new URL(request.url).origin,
|
||||
headers: request.headers
|
||||
export async function getApp(config: NextjsBkndConfig) {
|
||||
if (building) {
|
||||
while (building) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
}
|
||||
if (app) return app;
|
||||
}
|
||||
|
||||
building = true;
|
||||
if (!app) {
|
||||
app = await createFrameworkApp(config);
|
||||
await app.build();
|
||||
}
|
||||
building = false;
|
||||
return app;
|
||||
}
|
||||
|
||||
function getCleanRequest(req: Request, cleanRequest: NextjsBkndConfig["cleanRequest"]) {
|
||||
if (!cleanRequest) return req;
|
||||
|
||||
const url = new URL(req.url);
|
||||
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",
|
||||
});
|
||||
}
|
||||
|
||||
export function withApi<T>(handler: (ctx: GetServerSidePropsContext & { api: Api }) => T) {
|
||||
return async (ctx: GetServerSidePropsContext & { api: Api }) => {
|
||||
const api = createApi(ctx);
|
||||
await api.verifyAuth();
|
||||
return handler({ ...ctx, api });
|
||||
};
|
||||
}
|
||||
|
||||
function getCleanRequest(
|
||||
req: Request,
|
||||
{ cleanSearch = ["route"] }: Pick<NextjsBkndConfig, "cleanSearch">
|
||||
) {
|
||||
const url = new URL(req.url);
|
||||
cleanSearch?.forEach((k) => url.searchParams.delete(k));
|
||||
|
||||
return new Request(url.toString(), {
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
body: req.body
|
||||
body: req.body,
|
||||
});
|
||||
}
|
||||
|
||||
let app: App;
|
||||
export function serve({ cleanSearch, ...config }: NextjsBkndConfig = {}) {
|
||||
export function serve({ cleanRequest, ...config }: NextjsBkndConfig = {}) {
|
||||
return async (req: Request) => {
|
||||
if (!app) {
|
||||
app = await createFrameworkApp(config);
|
||||
app = await getApp(config);
|
||||
}
|
||||
const request = getCleanRequest(req, { cleanSearch });
|
||||
return app.fetch(request, process.env);
|
||||
const request = getCleanRequest(req, cleanRequest);
|
||||
return app.fetch(request);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { registries } from "bknd";
|
||||
import {
|
||||
type LocalAdapterConfig,
|
||||
StorageLocalAdapter
|
||||
StorageLocalAdapter,
|
||||
} from "../../media/storage/adapters/StorageLocalAdapter";
|
||||
|
||||
export * from "./node.adapter";
|
||||
|
||||
@@ -24,7 +24,7 @@ export function serve({
|
||||
}: NodeBkndConfig = {}) {
|
||||
const root = path.relative(
|
||||
process.cwd(),
|
||||
path.resolve(distPath ?? relativeDistPath ?? "./node_modules/bknd/dist", "static")
|
||||
path.resolve(distPath ?? relativeDistPath ?? "./node_modules/bknd/dist", "static"),
|
||||
);
|
||||
if (relativeDistPath) {
|
||||
console.warn("relativeDistPath is deprecated, please use distPath instead");
|
||||
@@ -41,16 +41,16 @@ export function serve({
|
||||
registerLocalMediaAdapter();
|
||||
app = await createRuntimeApp({
|
||||
...config,
|
||||
serveStatic: serveStatic({ root })
|
||||
serveStatic: serveStatic({ root }),
|
||||
});
|
||||
}
|
||||
|
||||
return app.fetch(req);
|
||||
}
|
||||
},
|
||||
},
|
||||
(connInfo) => {
|
||||
console.log(`Server is running on http://localhost:${connInfo.port}`);
|
||||
listener?.(connInfo);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,10 @@ type RemixContext = {
|
||||
let app: App;
|
||||
let building: boolean = false;
|
||||
|
||||
export async function getApp(config: RemixBkndConfig, args?: RemixContext) {
|
||||
export async function getApp<Args extends RemixContext = RemixContext>(
|
||||
config: RemixBkndConfig<Args>,
|
||||
args?: Args
|
||||
) {
|
||||
if (building) {
|
||||
while (building) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
@@ -28,10 +31,10 @@ export async function getApp(config: RemixBkndConfig, args?: RemixContext) {
|
||||
}
|
||||
|
||||
export function serve<Args extends RemixContext = RemixContext>(
|
||||
config: RemixBkndConfig<Args> = {}
|
||||
config: RemixBkndConfig<Args> = {},
|
||||
) {
|
||||
return async (args: Args) => {
|
||||
app = await createFrameworkApp(config, args);
|
||||
app = await getApp(config, args);
|
||||
return app.fetch(args.request);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,6 +20,6 @@ export function nodeRequestToRequest(req: IncomingMessage): Request {
|
||||
const method = req.method || "GET";
|
||||
return new Request(url, {
|
||||
method,
|
||||
headers
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export const devServerConfig = {
|
||||
/^\/@.+$/,
|
||||
/\/components.*?\.json.*/, // @todo: improve
|
||||
/^\/(public|assets|static)\/.+/,
|
||||
/^\/node_modules\/.*/
|
||||
/^\/node_modules\/.*/,
|
||||
] as any,
|
||||
injectClientScript: false
|
||||
injectClientScript: false,
|
||||
} as const;
|
||||
|
||||
@@ -24,7 +24,7 @@ window.__vite_plugin_react_preamble_installed__ = true
|
||||
</script>
|
||||
<script type="module" src="/@vite/client"></script>
|
||||
${addBkndContext ? "<!-- BKND_CONTEXT -->" : ""}
|
||||
</head>`
|
||||
</head>`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,12 +39,12 @@ async function createApp(config: ViteBkndConfig = {}, env?: any) {
|
||||
: {
|
||||
html: config.html,
|
||||
forceDev: config.forceDev ?? {
|
||||
mainPath: "/src/main.tsx"
|
||||
}
|
||||
mainPath: "/src/main.tsx",
|
||||
},
|
||||
serveStatic: ["/assets/*", serveStatic({ root: config.distPath ?? "./" })]
|
||||
},
|
||||
env
|
||||
serveStatic: ["/assets/*", serveStatic({ root: config.distPath ?? "./" })],
|
||||
},
|
||||
env,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export function serveFresh(config: Omit<ViteBkndConfig, "mode"> = {}) {
|
||||
async fetch(request: Request, env: any, ctx: ExecutionContext) {
|
||||
const app = await createApp(config, env);
|
||||
return app.fetch(request, env, ctx);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ export function serveCached(config: Omit<ViteBkndConfig, "mode"> = {}) {
|
||||
}
|
||||
|
||||
return app.fetch(request, env, ctx);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -77,6 +77,6 @@ export function serve({ mode, ...config }: ViteBkndConfig = {}) {
|
||||
export function devServer(options: DevServerOptions) {
|
||||
return honoViteDevServer({
|
||||
...devServerConfig,
|
||||
...options
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ import {
|
||||
Authenticator,
|
||||
type ProfileExchange,
|
||||
Role,
|
||||
type Strategy
|
||||
type Strategy,
|
||||
} from "auth";
|
||||
import type { PasswordStrategy } from "auth/authenticate/strategies";
|
||||
import { type DB, Exception, type PrimaryFieldType } from "core";
|
||||
import { $console, type DB, Exception, type PrimaryFieldType } from "core";
|
||||
import { type Static, secureRandomString, transformObject } from "core/utils";
|
||||
import type { Entity, EntityManager } from "data";
|
||||
import { type FieldSchema, em, entity, enumm, text } from "data/prototype";
|
||||
@@ -41,6 +41,12 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
}
|
||||
}
|
||||
|
||||
// @todo: password strategy is required atm
|
||||
if (!to.strategies?.password?.enabled) {
|
||||
$console.warn("Password strategy cannot be disabled.");
|
||||
to.strategies!.password!.enabled = true;
|
||||
}
|
||||
|
||||
return to;
|
||||
}
|
||||
|
||||
@@ -56,7 +62,6 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
|
||||
// register roles
|
||||
const roles = transformObject(this.config.roles ?? {}, (role, name) => {
|
||||
//console.log("role", role, name);
|
||||
return Role.create({ name, ...role });
|
||||
});
|
||||
this.ctx.guard.setRoles(Object.values(roles));
|
||||
@@ -69,15 +74,15 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Could not build strategy ${String(
|
||||
name
|
||||
)} with config ${JSON.stringify(strategy.config)}`
|
||||
name,
|
||||
)} with config ${JSON.stringify(strategy.config)}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this._authenticator = new Authenticator(strategies, this.resolveUser.bind(this), {
|
||||
jwt: this.config.jwt,
|
||||
cookie: this.config.cookie
|
||||
cookie: this.config.cookie,
|
||||
});
|
||||
|
||||
this.registerEntities();
|
||||
@@ -88,6 +93,14 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
this.ctx.guard.registerPermissions(Object.values(AuthPermissions));
|
||||
}
|
||||
|
||||
isStrategyEnabled(strategy: Strategy | string) {
|
||||
const name = typeof strategy === "string" ? strategy : strategy.getName();
|
||||
// for now, password is always active
|
||||
if (name === "password") return true;
|
||||
|
||||
return this.config.strategies?.[name]?.enabled ?? false;
|
||||
}
|
||||
|
||||
get controller(): AuthController {
|
||||
if (!this.isBuilt()) {
|
||||
throw new Error("Can't access controller, AppAuth not built yet");
|
||||
@@ -113,14 +126,8 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
action: AuthAction,
|
||||
strategy: Strategy,
|
||||
identifier: string,
|
||||
profile: ProfileExchange
|
||||
profile: ProfileExchange,
|
||||
): Promise<any> {
|
||||
/*console.log("***** AppAuth:resolveUser", {
|
||||
action,
|
||||
strategy: strategy.getName(),
|
||||
identifier,
|
||||
profile
|
||||
});*/
|
||||
if (!this.config.allow_register && action === "register") {
|
||||
throw new Exception("Registration is not allowed", 403);
|
||||
}
|
||||
@@ -129,7 +136,7 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
.getFillableFields("create")
|
||||
.map((f) => f.name);
|
||||
const filteredProfile = Object.fromEntries(
|
||||
Object.entries(profile).filter(([key]) => fields.includes(key))
|
||||
Object.entries(profile).filter(([key]) => fields.includes(key)),
|
||||
);
|
||||
|
||||
switch (action) {
|
||||
@@ -141,21 +148,10 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
}
|
||||
|
||||
private filterUserData(user: any) {
|
||||
/*console.log(
|
||||
"--filterUserData",
|
||||
user,
|
||||
this.config.jwt.fields,
|
||||
pick(user, this.config.jwt.fields)
|
||||
);*/
|
||||
return pick(user, this.config.jwt.fields);
|
||||
}
|
||||
|
||||
private async login(strategy: Strategy, identifier: string, profile: ProfileExchange) {
|
||||
/*console.log("--- trying to login", {
|
||||
strategy: strategy.getName(),
|
||||
identifier,
|
||||
profile
|
||||
});*/
|
||||
if (!("email" in profile)) {
|
||||
throw new Exception("Profile must have email");
|
||||
}
|
||||
@@ -172,18 +168,14 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
if (!result.data) {
|
||||
throw new Exception("User not found", 404);
|
||||
}
|
||||
//console.log("---login data", result.data, result);
|
||||
|
||||
// compare strategy and identifier
|
||||
//console.log("strategy comparison", result.data.strategy, strategy.getName());
|
||||
if (result.data.strategy !== strategy.getName()) {
|
||||
//console.log("!!! User registered with different strategy");
|
||||
throw new Exception("User registered with different strategy");
|
||||
}
|
||||
|
||||
//console.log("identifier comparison", result.data.strategy_value, identifier);
|
||||
if (result.data.strategy_value !== identifier) {
|
||||
//console.log("!!! Invalid credentials");
|
||||
throw new Exception("Invalid credentials");
|
||||
}
|
||||
|
||||
@@ -207,7 +199,7 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
const payload: any = {
|
||||
...profile,
|
||||
strategy: strategy.getName(),
|
||||
strategy_value: identifier
|
||||
strategy_value: identifier,
|
||||
};
|
||||
|
||||
const mutator = this.em.mutator(users);
|
||||
@@ -257,13 +249,13 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
email: text().required(),
|
||||
strategy: text({
|
||||
fillable: ["create"],
|
||||
hidden: ["update", "form"]
|
||||
hidden: ["update", "form"],
|
||||
}).required(),
|
||||
strategy_value: text({
|
||||
fillable: ["create"],
|
||||
hidden: ["read", "table", "update", "form"]
|
||||
hidden: ["read", "table", "update", "form"],
|
||||
}).required(),
|
||||
role: text()
|
||||
role: text(),
|
||||
};
|
||||
|
||||
registerEntities() {
|
||||
@@ -271,12 +263,12 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
this.ensureSchema(
|
||||
em(
|
||||
{
|
||||
[users.name as "users"]: users
|
||||
[users.name as "users"]: users,
|
||||
},
|
||||
({ index }, { users }) => {
|
||||
index(users).on(["email"], true).on(["strategy"]).on(["strategy_value"]);
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -285,6 +277,7 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
// also keep disabled strategies as a choice
|
||||
const strategies = Object.keys(this.config.strategies ?? {});
|
||||
this.replaceEntityField(users, "strategy", enumm({ enum: strategies }));
|
||||
} catch (e) {}
|
||||
@@ -304,7 +297,7 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
...(additional as any),
|
||||
email,
|
||||
strategy,
|
||||
strategy_value
|
||||
strategy_value,
|
||||
});
|
||||
mutator.__unstable_toggleSystemEntityCreation(true);
|
||||
return created;
|
||||
@@ -315,9 +308,16 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
return this.configDefault;
|
||||
}
|
||||
|
||||
const strategies = this.authenticator.getStrategies();
|
||||
|
||||
return {
|
||||
...this.config,
|
||||
...this.authenticator.toJSON(secrets)
|
||||
...this.authenticator.toJSON(secrets),
|
||||
strategies: transformObject(strategies, (strategy) => ({
|
||||
enabled: this.isStrategyEnabled(strategy),
|
||||
type: strategy.getType(),
|
||||
config: strategy.toJSON(secrets),
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user