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:
dswbx
2025-03-11 13:56:27 +01:00
498 changed files with 14118 additions and 5427 deletions

26
.github/workflows/test.yml vendored Normal file
View 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

View File

@@ -4,7 +4,7 @@
![bknd](https://raw.githubusercontent.com/bknd-io/bknd/refs/heads/main/docs/_assets/poster.png) ![bknd](https://raw.githubusercontent.com/bknd-io/bknd/refs/heads/main/docs/_assets/poster.png)
<p align="center" width="100%"> <p align="center" width="100%">
<a href="https://stackblitz.com/github/bknd-io/bknd-examples?hideExplorer=1&embed=1&view=preview&startScript=example-admin-rich&initialPath=%2Fdata%2Fschema"> <a href="https://stackblitz.com/github/bknd-io/bknd-examples?hideExplorer=1&embed=1&view=preview&startScript=example-admin-rich&initialPath=%2Fdata%2Fschema" target="_blank">
<strong>⭐ Live Demo</strong> <strong>⭐ Live Demo</strong>
</a> </a>
</p> </p>
@@ -18,13 +18,15 @@ bknd simplifies app development by providing a fully functional backend for data
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0. > and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
## Size ## Size
![gzipped size of bknd](https://img.badgesize.io/https://unpkg.com/bknd@0.7.1/dist/index.js?compression=gzip&label=bknd) ![gzipped size of bknd](https://img.badgesize.io/https://unpkg.com/bknd@latest/dist/index.js?compression=gzip&label=bknd)
![gzipped size of bknd/client](https://img.badgesize.io/https://unpkg.com/bknd@0.7.1/dist/ui/client/index.js?compression=gzip&label=bknd/client) ![gzipped size of bknd/client](https://img.badgesize.io/https://unpkg.com/bknd@latest/dist/ui/client/index.js?compression=gzip&label=bknd/client)
![gzipped size of bknd/elements](https://img.badgesize.io/https://unpkg.com/bknd@0.7.1/dist/ui/elements/index.js?compression=gzip&label=bknd/elements) ![gzipped size of bknd/elements](https://img.badgesize.io/https://unpkg.com/bknd@latest/dist/ui/elements/index.js?compression=gzip&label=bknd/elements)
![gzipped size of bknd/ui](https://img.badgesize.io/https://unpkg.com/bknd@0.7.1/dist/ui/index.js?compression=gzip&label=bknd/ui) ![gzipped size of bknd/ui](https://img.badgesize.io/https://unpkg.com/bknd@latest/dist/ui/index.js?compression=gzip&label=bknd/ui)
The size on npm is misleading, as the `bknd` package includes the backend, the ui components as well as the whole backend bundled into the cli including static assets. The size on npm is misleading, as the `bknd` package includes the backend, the ui components as well as the whole backend bundled into the cli including static assets.
Depending on what you use, the size can be higher as additional dependencies are getting pulled in. The minimal size of a full `bknd` app as an API is around 212 kB gzipped (e.g. deployed as Cloudflare Worker).
## Motivation ## Motivation
Creating digital products always requires developing both the backend (the logic) and the frontend (the appearance). Building a backend from scratch demands deep knowledge in areas such as authentication and database management. Using a backend framework can speed up initial development, but it still requires ongoing effort to work within its constraints (e.g., *"how to do X with Y?"*), which can quickly slow you down. Choosing a backend system is a tough decision, as you might not be aware of its limitations until you encounter them. Creating digital products always requires developing both the backend (the logic) and the frontend (the appearance). Building a backend from scratch demands deep knowledge in areas such as authentication and database management. Using a backend framework can speed up initial development, but it still requires ongoing effort to work within its constraints (e.g., *"how to do X with Y?"*), which can quickly slow you down. Choosing a backend system is a tough decision, as you might not be aware of its limitations until you encounter them.
@@ -78,7 +80,7 @@ export default function AdminPage() {
### Using the REST API or TypeScript SDK (`bknd/client`) ### Using the REST API or TypeScript SDK (`bknd/client`)
If you're not using a JavaScript environment, you can still access any endpoint using the REST API: If you're not using a JavaScript environment, you can still access any endpoint using the REST API:
```bash ```bash
curl -XGET <your-endpoint>/api/data/<entity> curl -XGET <your-endpoint>/api/data/entity/<entity>
{ {
"data": [ "data": [
{ "id": 1, ... }, { "id": 1, ... },

View File

@@ -19,8 +19,8 @@ describe("Api", async () => {
const token = await sign({ foo: "bar" }, "1234"); const token = await sign({ foo: "bar" }, "1234");
const request = new Request("http://example.com/test", { const request = new Request("http://example.com/test", {
headers: { headers: {
Authorization: `Bearer ${token}` Authorization: `Bearer ${token}`,
} },
}); });
const api = new Api({ request }); const api = new Api({ request });
expect(api.isAuthVerified()).toBe(false); expect(api.isAuthVerified()).toBe(false);
@@ -35,8 +35,8 @@ describe("Api", async () => {
const token = await sign({ foo: "bar" }, "1234"); const token = await sign({ foo: "bar" }, "1234");
const request = new Request("http://example.com/test", { const request = new Request("http://example.com/test", {
headers: { headers: {
Cookie: `auth=${token}` Cookie: `auth=${token}`,
} },
}); });
const api = new Api({ request }); const api = new Api({ request });
expect(api.isAuthVerified()).toBe(false); expect(api.isAuthVerified()).toBe(false);

View File

@@ -26,7 +26,7 @@ describe("DataApi", () => {
it("returns result", async () => { it("returns result", async () => {
const schema = proto.em({ const schema = proto.em({
posts: proto.entity("posts", { title: proto.text() }) posts: proto.entity("posts", { title: proto.text() }),
}); });
const em = schemaToEm(schema); const em = schemaToEm(schema);
await em.schema().sync({ force: true }); await em.schema().sync({ force: true });
@@ -40,19 +40,17 @@ describe("DataApi", () => {
{ {
const res = (await app.request("/entity/posts")) as Response; const res = (await app.request("/entity/posts")) as Response;
const { data } = await res.json(); const { data } = (await res.json()) as any;
expect(data.length).toEqual(3); expect(data.length).toEqual(3);
} }
// @ts-ignore tests // @ts-ignore tests
const api = new DataApi({ basepath: "/", queryLengthLimit: 50 }); const api = new DataApi({ basepath: "/", queryLengthLimit: 50 }, app.request as typeof fetch);
// @ts-ignore protected
api.fetcher = app.request as typeof fetch;
{ {
const req = api.readMany("posts", { select: ["title"] }); const req = api.readMany("posts", { select: ["title"] });
expect(req.request.method).toBe("GET"); expect(req.request.method).toBe("GET");
const res = await req; const res = await req;
expect(res.data).toEqual(payload); expect(res.data).toEqual(payload as any);
} }
{ {
@@ -60,11 +58,155 @@ describe("DataApi", () => {
select: ["title"], select: ["title"],
limit: 100000, limit: 100000,
offset: 0, offset: 0,
sort: "id" sort: "id",
}); });
expect(req.request.method).toBe("POST"); expect(req.request.method).toBe("POST");
const res = await req; const res = await req;
expect(res.data).toEqual(payload); expect(res.data).toEqual(payload as any);
}
});
it("updates many", async () => {
const schema = proto.em({
posts: proto.entity("posts", { title: proto.text(), count: proto.number() }),
});
const em = schemaToEm(schema);
await em.schema().sync({ force: true });
const payload = [
{ title: "foo", count: 0 },
{ title: "bar", count: 0 },
{ title: "baz", count: 0 },
{ title: "bla", count: 2 },
];
await em.mutator("posts").insertMany(payload);
const ctx: any = { em, guard: new Guard() };
const controller = new DataController(ctx, dataConfig);
const app = controller.getController();
// @ts-ignore tests
const api = new DataApi({ basepath: "/" }, app.request as typeof fetch);
{
const req = api.readMany("posts", {
select: ["title", "count"],
});
const res = await req;
expect(res.data).toEqual(payload as any);
}
{
// update with empty where
expect(() => api.updateMany("posts", {}, { count: 1 })).toThrow();
expect(() => api.updateMany("posts", undefined, { count: 1 })).toThrow();
}
{
// update
const req = await api.updateMany("posts", { count: 0 }, { count: 1 });
expect(req.res.status).toBe(200);
}
{
// compare
const res = await api.readMany("posts", {
select: ["title", "count"],
});
expect(res.map((p) => p.count)).toEqual([1, 1, 1, 2]);
}
});
it("refines", async () => {
const schema = proto.em({
posts: proto.entity("posts", { title: proto.text() }),
});
const em = schemaToEm(schema);
await em.schema().sync({ force: true });
const payload = [{ title: "foo" }, { title: "bar" }, { title: "baz" }];
await em.mutator("posts").insertMany(payload);
const ctx: any = { em, guard: new Guard() };
const controller = new DataController(ctx, dataConfig);
const app = controller.getController();
const api = new DataApi({ basepath: "/" }, app.request as typeof fetch);
const normalOne = api.readOne("posts", 1);
const normal = api.readMany("posts", { select: ["title"], where: { title: "baz" } });
expect((await normal).data).toEqual([{ title: "baz" }] as any);
// refine
const refined = normal.refine((data) => data[0]);
expect((await refined).data).toEqual({ title: "baz" } as any);
// one
const oneBy = api.readOneBy("posts", { where: { title: "baz" }, select: ["title"] });
const oneByRes = await oneBy;
expect(oneByRes.data).toEqual({ title: "baz" } as any);
expect(oneByRes.body.meta.count).toEqual(1);
});
it("exists/count", async () => {
const schema = proto.em({
posts: proto.entity("posts", { title: proto.text() }),
});
const em = schemaToEm(schema);
await em.schema().sync({ force: true });
const payload = [{ title: "foo" }, { title: "bar" }, { title: "baz" }];
await em.mutator("posts").insertMany(payload);
const ctx: any = { em, guard: new Guard() };
const controller = new DataController(ctx, dataConfig);
const app = controller.getController();
const api = new DataApi({ basepath: "/" }, app.request as typeof fetch);
const exists = api.exists("posts", { id: 1 });
expect((await exists).exists).toBeTrue();
expect((await api.count("posts")).count).toEqual(3);
});
it("creates many", async () => {
const schema = proto.em({
posts: proto.entity("posts", { title: proto.text(), count: proto.number() }),
});
const em = schemaToEm(schema);
await em.schema().sync({ force: true });
const payload = [
{ title: "foo", count: 0 },
{ title: "bar", count: 0 },
{ title: "baz", count: 0 },
{ title: "bla", count: 2 },
];
const ctx: any = { em, guard: new Guard() };
const controller = new DataController(ctx, dataConfig);
const app = controller.getController();
// @ts-ignore tests
const api = new DataApi({ basepath: "/" }, app.request as typeof fetch);
{
// create many
const res = await api.createMany("posts", payload);
expect(res.data.length).toEqual(4);
expect(res.ok).toBeTrue();
}
{
const req = api.readMany("posts", {
select: ["title", "count"],
});
const res = await req;
expect(res.data).toEqual(payload as any);
}
{
// create with empty
expect(() => api.createMany("posts", [])).toThrow();
} }
}); });
}); });

View File

@@ -18,8 +18,8 @@ const mockedBackend = new Hono()
return new Response(file, { return new Response(file, {
headers: { headers: {
"Content-Type": file.type, "Content-Type": file.type,
"Content-Length": file.size.toString() "Content-Length": file.size.toString(),
} },
}); });
}); });
@@ -30,15 +30,15 @@ describe("MediaApi", () => {
// @ts-ignore tests // @ts-ignore tests
const api = new MediaApi({ const api = new MediaApi({
host, host,
basepath basepath,
}); });
expect(api.getFileUploadUrl({ path: "path" })).toBe(`${host}${basepath}/upload/path`); expect(api.getFileUploadUrl({ path: "path" } as any)).toBe(`${host}${basepath}/upload/path`);
}); });
it("should have correct upload headers", () => { it("should have correct upload headers", () => {
// @ts-ignore tests // @ts-ignore tests
const api = new MediaApi({ const api = new MediaApi({
token: "token" token: "token",
}); });
expect(api.getUploadHeaders().get("Authorization")).toBe("Bearer token"); expect(api.getUploadHeaders().get("Authorization")).toBe("Bearer token");
}); });
@@ -139,7 +139,7 @@ describe("MediaApi", () => {
const response = (await mockedBackend.request(url)) as Response; const response = (await mockedBackend.request(url)) as Response;
await matches( await matches(
await api.upload(response.body!, { filename: "readable.png" }), await api.upload(response.body!, { filename: "readable.png" }),
"readable.png" "readable.png",
); );
} }
}); });

View File

@@ -61,7 +61,7 @@ describe("ModuleApi", () => {
it("adds additional headers from options", () => { it("adds additional headers from options", () => {
const headers = new Headers({ const headers = new Headers({
"X-Test": "123" "X-Test": "123",
}); });
const api = new Api({ host, headers }); const api = new Api({ host, headers });
expect(api.get("/").request.headers.get("X-Test")).toEqual("123"); expect(api.get("/").request.headers.get("X-Test")).toEqual("123");
@@ -75,7 +75,7 @@ describe("ModuleApi", () => {
it("uses search params", () => { it("uses search params", () => {
const api = new Api({ host }); const api = new Api({ host });
const search = new URLSearchParams({ const search = new URLSearchParams({
foo: "bar" foo: "bar",
}); });
expect(api.get("/", search).request.url).toEqual("http://localhost/?" + search.toString()); expect(api.get("/", search).request.url).toEqual("http://localhost/?" + search.toString());
}); });
@@ -89,6 +89,14 @@ describe("ModuleApi", () => {
expect(api.delete("/").request.method).toEqual("DELETE"); expect(api.delete("/").request.method).toEqual("DELETE");
}); });
it("refines", async () => {
const app = new Hono().get("/endpoint", (c) => c.json({ foo: ["bar"] }));
const api = new Api({ host }, app.request as typeof fetch);
expect((await api.get("/endpoint")).data).toEqual({ foo: ["bar"] });
expect((await api.get("/endpoint").refine((data) => data.foo)).data).toEqual(["bar"]);
});
// @todo: test error response // @todo: test error response
// @todo: test method shortcut functions // @todo: test method shortcut functions
}); });

View 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");
});
});

View File

@@ -24,9 +24,9 @@ describe("repros", async () => {
adapter: { adapter: {
type: "local", type: "local",
config: { config: {
path: "./" path: "./",
} },
} },
}); });
expect(config.enabled).toBe(true); expect(config.enabled).toBe(true);
@@ -38,9 +38,9 @@ describe("repros", async () => {
"entities.test", "entities.test",
proto proto
.entity("test", { .entity("test", {
content: proto.text() content: proto.text(),
}) })
.toJSON() .toJSON(),
); );
expect(app.em.entities.map((e) => e.name)).toContain("test"); expect(app.em.entities.map((e) => e.name)).toContain("test");
} }
@@ -54,8 +54,8 @@ describe("repros", async () => {
hidden: false, hidden: false,
mime_types: [], mime_types: [],
virtual: true, virtual: true,
entity: "test" entity: "test",
} },
}); });
expect( expect(
@@ -63,8 +63,8 @@ describe("repros", async () => {
type: "poly", type: "poly",
source: "test", source: "test",
target: "media", target: "media",
config: { mappedBy: "files" } config: { mappedBy: "files" },
}) }),
).resolves.toBeDefined(); ).resolves.toBeDefined();
} }
@@ -75,17 +75,17 @@ describe("repros", async () => {
const schema = proto.em( const schema = proto.em(
{ {
products: proto.entity("products", { products: proto.entity("products", {
title: proto.text() title: proto.text(),
}), }),
product_likes: proto.entity("product_likes", { 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, schema) => {
fns.relation(schema.product_likes).manyToOne(schema.products, { inversedBy: "likes" }); fns.relation(schema.product_likes).manyToOne(schema.products, { inversedBy: "likes" });
fns.relation(schema.product_likes).manyToOne(schema.users); fns.relation(schema.product_likes).manyToOne(schema.users);
} },
); );
const app = createApp({ initialConfig: { data: schema.toJSON() } }); const app = createApp({ initialConfig: { data: schema.toJSON() } });
await app.build(); await app.build();
@@ -96,8 +96,8 @@ describe("repros", async () => {
expect(info.relations.listable).toEqual([ expect(info.relations.listable).toEqual([
{ {
entity: "product_likes", entity: "product_likes",
ref: "likes" ref: "likes",
} },
]); ]);
}); });
}); });

View File

@@ -7,13 +7,13 @@ describe("authorize", () => {
["read", "write"], ["read", "write"],
{ {
admin: { admin: {
permissions: ["read", "write"] permissions: ["read", "write"],
} },
}, },
{ enabled: true } { enabled: true },
); );
const user = { const user = {
role: "admin" role: "admin",
}; };
expect(guard.granted("read", user)).toBe(true); expect(guard.granted("read", user)).toBe(true);
@@ -27,21 +27,21 @@ describe("authorize", () => {
["read", "write"], ["read", "write"],
{ {
admin: { admin: {
permissions: ["read", "write"] permissions: ["read", "write"],
}, },
guest: { guest: {
permissions: ["read"], permissions: ["read"],
is_default: true is_default: true,
} },
}, },
{ enabled: true } { enabled: true },
); );
expect(guard.granted("read")).toBe(true); expect(guard.granted("read")).toBe(true);
expect(guard.granted("write")).toBe(false); expect(guard.granted("write")).toBe(false);
const user = { const user = {
role: "admin" role: "admin",
}; };
expect(guard.granted("read", user)).toBe(true); expect(guard.granted("read", user)).toBe(true);
@@ -58,12 +58,12 @@ describe("authorize", () => {
test("role implicit allow", async () => { test("role implicit allow", async () => {
const guard = Guard.create(["read", "write"], { const guard = Guard.create(["read", "write"], {
admin: { admin: {
implicit_allow: true implicit_allow: true,
} },
}); });
const user = { const user = {
role: "admin" role: "admin",
}; };
expect(guard.granted("read", user)).toBe(true); expect(guard.granted("read", user)).toBe(true);
@@ -74,8 +74,8 @@ describe("authorize", () => {
const guard = Guard.create(["read", "write"], { const guard = Guard.create(["read", "write"], {
guest: { guest: {
implicit_allow: true, implicit_allow: true,
is_default: true is_default: true,
} },
}); });
expect(guard.getUserRole()?.name).toBe("guest"); expect(guard.getUserRole()?.name).toBe("guest");

View File

@@ -7,10 +7,10 @@ describe("OAuthStrategy", async () => {
const strategy = new OAuthStrategy({ const strategy = new OAuthStrategy({
type: "oidc", type: "oidc",
client: { client: {
client_id: process.env.OAUTH_CLIENT_ID, client_id: process.env.OAUTH_CLIENT_ID!,
client_secret: process.env.OAUTH_CLIENT_SECRET client_secret: process.env.OAUTH_CLIENT_SECRET!,
}, },
name: "google" name: "google",
}); });
const state = "---"; const state = "---";
const redirect_uri = "http://localhost:3000/auth/google/callback"; const redirect_uri = "http://localhost:3000/auth/google/callback";
@@ -19,11 +19,6 @@ describe("OAuthStrategy", async () => {
const config = await strategy.getConfig(); const config = await strategy.getConfig();
console.log("config", JSON.stringify(config, null, 2)); console.log("config", JSON.stringify(config, null, 2));
const request = await strategy.request({
redirect_uri,
state
});
const server = Bun.serve({ const server = Bun.serve({
fetch: async (req) => { fetch: async (req) => {
const url = new URL(req.url); const url = new URL(req.url);
@@ -31,13 +26,18 @@ describe("OAuthStrategy", async () => {
console.log("req", req); console.log("req", req);
const user = await strategy.callback(url, { const user = await strategy.callback(url, {
redirect_uri, redirect_uri,
state state,
}); });
console.log("---user", user); console.log("---user", user);
} }
return new Response("Bun!"); return new Response("Bun!");
} },
});
const request = await strategy.request({
redirect_uri,
state,
}); });
console.log("request", request); console.log("request", request);

View File

@@ -26,7 +26,7 @@ class ReturnEvent extends Event<{ foo: string }, string> {
} }
return this.clone({ return this.clone({
foo: [this.params.foo, value].join("-") foo: [this.params.foo, value].join("-"),
}); });
} }
} }
@@ -52,7 +52,7 @@ describe("EventManager", async () => {
await new Promise((resolve) => setTimeout(resolve, 50)); await new Promise((resolve) => setTimeout(resolve, 50));
delayed(); delayed();
}, },
"sync" "sync",
); );
// don't allow unknown // don't allow unknown
@@ -83,8 +83,8 @@ describe("EventManager", async () => {
const emgr = new EventManager( const emgr = new EventManager(
{ InformationalEvent }, { InformationalEvent },
{ {
asyncExecutor asyncExecutor,
} },
); );
emgr.onEvent(InformationalEvent, async () => {}); emgr.onEvent(InformationalEvent, async () => {});
@@ -98,8 +98,8 @@ describe("EventManager", async () => {
const emgr = new EventManager( const emgr = new EventManager(
{ ReturnEvent, InformationalEvent }, { ReturnEvent, InformationalEvent },
{ {
onInvalidReturn onInvalidReturn,
} },
); );
// @ts-expect-error InformationalEvent has no return value // @ts-expect-error InformationalEvent has no return value
@@ -140,7 +140,7 @@ describe("EventManager", async () => {
expect(slug).toBe("informational-event"); expect(slug).toBe("informational-event");
call(); call();
}, },
{ mode: "sync", once: true } { mode: "sync", once: true },
); );
expect(emgr.getListeners().length).toBe(1); expect(emgr.getListeners().length).toBe(1);

View File

@@ -30,8 +30,8 @@ describe("Registry", () => {
first: { first: {
cls: What, cls: What,
schema: Type.Object({ type: Type.String(), what: Type.String() }), schema: Type.Object({ type: Type.String(), what: Type.String() }),
enabled: true enabled: true,
} },
} satisfies Record<string, Test1>); } satisfies Record<string, Test1>);
const item = registry.get("first"); const item = registry.get("first");
@@ -42,7 +42,7 @@ describe("Registry", () => {
registry.add("second", { registry.add("second", {
cls: What2, cls: What2,
schema: second, schema: second,
enabled: true enabled: true,
}); });
// @ts-ignore // @ts-ignore
expect(registry.get("second").schema).toEqual(second); expect(registry.get("second").schema).toEqual(second);
@@ -52,7 +52,7 @@ describe("Registry", () => {
// @ts-expect-error // @ts-expect-error
cls: NotAllowed, cls: NotAllowed,
schema: third, schema: third,
enabled: true enabled: true,
}); });
// @ts-ignore // @ts-ignore
expect(registry.get("third").schema).toEqual(third); expect(registry.get("third").schema).toEqual(third);
@@ -62,7 +62,7 @@ describe("Registry", () => {
cls: What, cls: What,
// @ts-expect-error // @ts-expect-error
schema: fourth, schema: fourth,
enabled: true enabled: true,
}); });
// @ts-ignore // @ts-ignore
expect(registry.get("fourth").schema).toEqual(fourth); expect(registry.get("fourth").schema).toEqual(fourth);
@@ -75,7 +75,7 @@ describe("Registry", () => {
return { return {
cls: a, cls: a,
schema: a.prototype.getType(), schema: a.prototype.getType(),
enabled: true enabled: true,
}; };
}); });

View File

@@ -4,7 +4,7 @@ import { after, beforeEach, describe, test } from "node:test";
import { Miniflare } from "miniflare"; import { Miniflare } from "miniflare";
import { import {
CloudflareKVCacheItem, CloudflareKVCacheItem,
CloudflareKVCachePool CloudflareKVCachePool,
} from "../../../src/core/cache/adapters/CloudflareKvCache"; } from "../../../src/core/cache/adapters/CloudflareKvCache";
import { runTests } from "./cache-test-suite"; import { runTests } from "./cache-test-suite";
@@ -26,7 +26,7 @@ describe("CloudflareKv", async () => {
mf = new Miniflare({ mf = new Miniflare({
modules: true, modules: true,
script: "export default { async fetch() { return new Response(null); } }", script: "export default { async fetch() { return new Response(null); } }",
kvNamespaces: ["TEST"] kvNamespaces: ["TEST"],
}); });
const kv = await mf.getKVNamespace("TEST"); const kv = await mf.getKVNamespace("TEST");
return new CloudflareKVCachePool(kv as any); return new CloudflareKVCachePool(kv as any);
@@ -45,10 +45,10 @@ describe("CloudflareKv", async () => {
}, },
toBeUndefined() { toBeUndefined() {
assert.equal(actual, undefined); assert.equal(actual, undefined);
} },
}; };
} },
} },
}); });
after(async () => { after(async () => {

View File

@@ -9,7 +9,7 @@ describe("MemoryCache", () => {
tester: { tester: {
test, test,
beforeEach, beforeEach,
expect expect,
} },
}); });
}); });

View File

@@ -4,7 +4,7 @@ import { checksum, hash } from "../../src/core/utils";
describe("crypto", async () => { describe("crypto", async () => {
test("sha256", async () => { test("sha256", async () => {
expect(await hash.sha256("test")).toBe( expect(await hash.sha256("test")).toBe(
"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
); );
}); });
test("sha1", async () => { test("sha1", async () => {

View 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();
});
});

View File

@@ -8,8 +8,8 @@ describe("SchemaObject", async () => {
Type.Object({ a: Type.String({ default: "b" }) }), Type.Object({ a: Type.String({ default: "b" }) }),
{ a: "test" }, { a: "test" },
{ {
forceParse: true forceParse: true,
} },
); );
expect(m.get()).toEqual({ a: "test" }); expect(m.get()).toEqual({ a: "test" });
@@ -30,14 +30,14 @@ describe("SchemaObject", async () => {
b: Type.Object( b: Type.Object(
{ {
c: Type.String({ default: "d" }), c: Type.String({ default: "d" }),
e: Type.String({ default: "f" }) e: Type.String({ default: "f" }),
}, },
{ default: {} } { default: {} },
) ),
}, },
{ default: {}, additionalProperties: false } { default: {}, additionalProperties: false },
) ),
}) }),
); );
expect(m.get()).toEqual({ s: { a: "b", b: { c: "d", e: "f" } } }); expect(m.get()).toEqual({ s: { a: "b", b: { c: "d", e: "f" } } });
@@ -59,8 +59,8 @@ describe("SchemaObject", async () => {
test("patch array", async () => { test("patch array", async () => {
const m = new SchemaObject( const m = new SchemaObject(
Type.Object({ Type.Object({
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }) methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
}) }),
); );
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] }); expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
@@ -81,14 +81,14 @@ describe("SchemaObject", async () => {
a: Type.String({ default: "b" }), a: Type.String({ default: "b" }),
b: Type.Object( b: Type.Object(
{ {
c: Type.String({ default: "d" }) c: Type.String({ default: "d" }),
}, },
{ default: {} } { default: {} },
) ),
}, },
{ default: {} } { default: {} },
) ),
}) }),
); );
expect(m.get()).toEqual({ s: { a: "b", b: { c: "d" } } }); expect(m.get()).toEqual({ s: { a: "b", b: { c: "d" } } });
@@ -108,8 +108,8 @@ describe("SchemaObject", async () => {
test("set", async () => { test("set", async () => {
const m = new SchemaObject( const m = new SchemaObject(
Type.Object({ Type.Object({
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }) methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
}) }),
); );
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] }); expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
@@ -125,7 +125,7 @@ describe("SchemaObject", async () => {
let result: any; let result: any;
const m = new SchemaObject( const m = new SchemaObject(
Type.Object({ Type.Object({
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }) methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
}), }),
undefined, undefined,
{ {
@@ -133,8 +133,8 @@ describe("SchemaObject", async () => {
await new Promise((r) => setTimeout(r, 10)); await new Promise((r) => setTimeout(r, 10));
called = true; called = true;
result = config; result = config;
} },
} },
); );
await m.set({ methods: ["GET", "POST"] }); await m.set({ methods: ["GET", "POST"] });
@@ -146,7 +146,7 @@ describe("SchemaObject", async () => {
let called = false; let called = false;
const m = new SchemaObject( const m = new SchemaObject(
Type.Object({ Type.Object({
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }) methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
}), }),
undefined, undefined,
{ {
@@ -155,8 +155,8 @@ describe("SchemaObject", async () => {
called = true; called = true;
to.methods.push("OPTIONS"); to.methods.push("OPTIONS");
return to; return to;
} },
} },
); );
const result = await m.set({ methods: ["GET", "POST"] }); const result = await m.set({ methods: ["GET", "POST"] });
@@ -168,7 +168,7 @@ describe("SchemaObject", async () => {
test("throwIfRestricted", async () => { test("throwIfRestricted", async () => {
const m = new SchemaObject(Type.Object({}), undefined, { const m = new SchemaObject(Type.Object({}), undefined, {
restrictPaths: ["a.b"] restrictPaths: ["a.b"],
}); });
expect(() => m.throwIfRestricted("a.b")).toThrow(); expect(() => m.throwIfRestricted("a.b")).toThrow();
@@ -185,18 +185,18 @@ describe("SchemaObject", async () => {
a: Type.String({ default: "b" }), a: Type.String({ default: "b" }),
b: Type.Object( b: Type.Object(
{ {
c: Type.String({ default: "d" }) c: Type.String({ default: "d" }),
}, },
{ default: {} } { default: {} },
) ),
}, },
{ default: {} } { default: {} },
) ),
}), }),
undefined, undefined,
{ {
restrictPaths: ["s.b"] restrictPaths: ["s.b"],
} },
); );
expect(m.patch("s.b.c", "e")).rejects.toThrow(); expect(m.patch("s.b.c", "e")).rejects.toThrow();
@@ -217,33 +217,33 @@ describe("SchemaObject", async () => {
additionalProperties: Type.Object({ additionalProperties: Type.Object({
type: Type.String(), type: Type.String(),
config: Type.Optional( config: Type.Optional(
Type.Object({}, { additionalProperties: Type.String() }) Type.Object({}, { additionalProperties: Type.String() }),
) ),
}) }),
} },
), ),
config: Type.Optional(Type.Object({}, { additionalProperties: Type.String() })) config: Type.Optional(Type.Object({}, { additionalProperties: Type.String() })),
}) }),
} },
) ),
}, },
{ {
additionalProperties: false additionalProperties: false,
} },
); );
test("patch safe object, overwrite", async () => { test("patch safe object, overwrite", async () => {
const data = { const data = {
entities: { entities: {
some: { some: {
fields: { fields: {
a: { type: "string", config: { some: "thing" } } a: { type: "string", config: { some: "thing" } },
} },
} },
} },
}; };
const m = new SchemaObject(dataEntitiesSchema, data, { const m = new SchemaObject(dataEntitiesSchema, data, {
forceParse: true, forceParse: true,
overwritePaths: [/^entities\..*\.fields\..*\.config/] overwritePaths: [/^entities\..*\.fields\..*\.config/],
}); });
await m.patch("entities.some.fields.a", { type: "string", config: { another: "one" } }); await m.patch("entities.some.fields.a", { type: "string", config: { another: "one" } });
@@ -252,10 +252,10 @@ describe("SchemaObject", async () => {
entities: { entities: {
some: { some: {
fields: { fields: {
a: { type: "string", config: { another: "one" } } a: { type: "string", config: { another: "one" } },
} },
} },
} },
}); });
}); });
@@ -265,22 +265,22 @@ describe("SchemaObject", async () => {
users: { users: {
fields: { fields: {
email: { type: "string" }, email: { type: "string" },
password: { type: "string" } password: { type: "string" },
} },
} },
} },
}; };
const m = new SchemaObject(dataEntitiesSchema, data, { const m = new SchemaObject(dataEntitiesSchema, data, {
forceParse: true, forceParse: true,
overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/] overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/],
}); });
await m.patch("entities.test", { await m.patch("entities.test", {
fields: { fields: {
content: { content: {
type: "text" type: "text",
} },
} },
}); });
expect(m.get()).toEqual({ expect(m.get()).toEqual({
@@ -288,17 +288,17 @@ describe("SchemaObject", async () => {
users: { users: {
fields: { fields: {
email: { type: "string" }, email: { type: "string" },
password: { type: "string" } password: { type: "string" },
} },
}, },
test: { test: {
fields: { fields: {
content: { content: {
type: "text" type: "text",
} },
} },
} },
} },
}); });
}); });
@@ -308,14 +308,14 @@ describe("SchemaObject", async () => {
users: { users: {
fields: { fields: {
email: { type: "string" }, email: { type: "string" },
password: { type: "string" } password: { type: "string" },
} },
} },
} },
}; };
const m = new SchemaObject(dataEntitiesSchema, data, { const m = new SchemaObject(dataEntitiesSchema, data, {
forceParse: true, forceParse: true,
overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/] overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/],
}); });
expect(m.patch("desc", "entities.users.config.sort_dir")).rejects.toThrow(); expect(m.patch("desc", "entities.users.config.sort_dir")).rejects.toThrow();
@@ -323,13 +323,13 @@ describe("SchemaObject", async () => {
await m.patch("entities.test", { await m.patch("entities.test", {
fields: { fields: {
content: { content: {
type: "text" type: "text",
} },
} },
}); });
await m.patch("entities.users.config", { await m.patch("entities.users.config", {
sort_dir: "desc" sort_dir: "desc",
}); });
expect(m.get()).toEqual({ expect(m.get()).toEqual({
@@ -337,20 +337,20 @@ describe("SchemaObject", async () => {
users: { users: {
fields: { fields: {
email: { type: "string" }, email: { type: "string" },
password: { type: "string" } password: { type: "string" },
}, },
config: { config: {
sort_dir: "desc" sort_dir: "desc",
} },
}, },
test: { test: {
fields: { fields: {
content: { content: {
type: "text" type: "text",
} },
} },
} },
} },
}); });
}); });
}); });

View File

@@ -13,8 +13,8 @@ describe("diff", () => {
t: "a", t: "a",
p: ["b"], p: ["b"],
o: undefined, o: undefined,
n: 2 n: 2,
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -35,8 +35,8 @@ describe("diff", () => {
t: "r", t: "r",
p: ["b"], p: ["b"],
o: 2, o: 2,
n: undefined n: undefined,
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -57,8 +57,8 @@ describe("diff", () => {
t: "e", t: "e",
p: ["a"], p: ["a"],
o: 1, o: 1,
n: 2 n: 2,
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -79,8 +79,8 @@ describe("diff", () => {
t: "e", t: "e",
p: ["a", "b"], p: ["a", "b"],
o: 1, o: 1,
n: 2 n: 2,
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -101,14 +101,14 @@ describe("diff", () => {
t: "e", t: "e",
p: ["a", 1], p: ["a", 1],
o: 2, o: 2,
n: 4 n: 4,
}, },
{ {
t: "a", t: "a",
p: ["a", 3], p: ["a", 3],
o: undefined, o: undefined,
n: 5 n: 5,
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -129,20 +129,20 @@ describe("diff", () => {
t: "a", t: "a",
p: ["a", 0], p: ["a", 0],
o: undefined, o: undefined,
n: 1 n: 1,
}, },
{ {
t: "a", t: "a",
p: ["a", 1], p: ["a", 1],
o: undefined, o: undefined,
n: 2 n: 2,
}, },
{ {
t: "a", t: "a",
p: ["a", 2], p: ["a", 2],
o: undefined, o: undefined,
n: 3 n: 3,
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -163,14 +163,14 @@ describe("diff", () => {
t: "e", t: "e",
p: ["a", 1], p: ["a", 1],
o: 2, o: 2,
n: 3 n: 3,
}, },
{ {
t: "r", t: "r",
p: ["a", 2], p: ["a", 2],
o: 3, o: 3,
n: undefined n: undefined,
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -183,14 +183,14 @@ describe("diff", () => {
it("should handle complex nested changes", () => { it("should handle complex nested changes", () => {
const oldObj = { const oldObj = {
a: { a: {
b: [1, 2, { c: 3 }] b: [1, 2, { c: 3 }],
} },
}; };
const newObj = { const newObj = {
a: { a: {
b: [1, 2, { c: 4 }, 5] b: [1, 2, { c: 4 }, 5],
} },
}; };
const diffs = diff(oldObj, newObj); const diffs = diff(oldObj, newObj);
@@ -200,14 +200,14 @@ describe("diff", () => {
t: "e", t: "e",
p: ["a", "b", 2, "c"], p: ["a", "b", 2, "c"],
o: 3, o: 3,
n: 4 n: 4,
}, },
{ {
t: "a", t: "a",
p: ["a", "b", 3], p: ["a", "b", 3],
o: undefined, o: undefined,
n: 5 n: 5,
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -228,14 +228,14 @@ describe("diff", () => {
t: "e", t: "e",
p: ["a"], p: ["a"],
o: undefined, o: undefined,
n: null n: null,
}, },
{ {
t: "e", t: "e",
p: ["b"], p: ["b"],
o: null, o: null,
n: undefined n: undefined,
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -256,8 +256,8 @@ describe("diff", () => {
t: "e", t: "e",
p: ["a"], p: ["a"],
o: 1, o: 1,
n: "1" n: "1",
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -278,14 +278,14 @@ describe("diff", () => {
t: "r", t: "r",
p: ["b"], p: ["b"],
o: 2, o: 2,
n: undefined n: undefined,
}, },
{ {
t: "a", t: "a",
p: ["c"], p: ["c"],
o: undefined, o: undefined,
n: 3 n: 3,
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -306,8 +306,8 @@ describe("diff", () => {
t: "e", t: "e",
p: ["a"], p: ["a"],
o: [1, 2, 3], o: [1, 2, 3],
n: { b: 4 } n: { b: 4 },
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -328,8 +328,8 @@ describe("diff", () => {
t: "e", t: "e",
p: ["a"], p: ["a"],
o: { b: 1 }, o: { b: 1 },
n: 2 n: 2,
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -350,14 +350,14 @@ describe("diff", () => {
t: "r", t: "r",
p: ["a"], p: ["a"],
o: 1, o: 1,
n: undefined n: undefined,
}, },
{ {
t: "a", t: "a",
p: ["b"], p: ["b"],
o: undefined, o: undefined,
n: 2 n: 2,
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -408,8 +408,8 @@ describe("diff", () => {
t: "a", t: "a",
p: ["a"], p: ["a"],
o: undefined, o: undefined,
n: 1 n: 1,
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -430,8 +430,8 @@ describe("diff", () => {
t: "r", t: "r",
p: ["a"], p: ["a"],
o: 1, o: 1,
n: undefined n: undefined,
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);

View File

@@ -9,7 +9,7 @@ describe("object-query", () => {
test("validates", async () => { test("validates", async () => {
const converted = convert({ const converted = convert({
name: { $eq: "ch" } name: { $eq: "ch" },
}); });
validate(converted, { name: "Michael" }); validate(converted, { name: "Michael" });
}); });
@@ -31,7 +31,7 @@ describe("object-query", () => {
[{ val: { $notnull: 1 } }, { val: null }, false], [{ val: { $notnull: 1 } }, { val: null }, false],
[{ val: { $regex: ".*" } }, { val: "test" }, true], [{ val: { $regex: ".*" } }, { val: "test" }, true],
[{ val: { $regex: /^t.*/ } }, { val: "test" }, true], [{ val: { $regex: /^t.*/ } }, { val: "test" }, true],
[{ val: { $regex: /^b.*/ } }, { val: "test" }, false] [{ val: { $regex: /^b.*/ } }, { val: "test" }, false],
]; ];
for (const [query, object, expected] of tests) { for (const [query, object, expected] of tests) {
@@ -55,10 +55,10 @@ describe("object-query", () => {
[ [
{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } }, { $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } },
{ val1: "foo", val2: "bar" }, { val1: "foo", val2: "bar" },
true true,
], ],
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, { val1: 1 }, true], [{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, { val1: 1 }, true],
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, { val1: 3 }, false] [{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, { val1: 3 }, false],
]; ];
for (const [query, object, expected] of tests) { for (const [query, object, expected] of tests) {

View File

@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"; 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"; import * as utils from "../../src/core/utils";
async function wait(ms: number) { async function wait(ms: number) {
@@ -16,7 +16,7 @@ describe("Core Utils", async () => {
expect(result).toEqual([ expect(result).toEqual([
{ key: "a", value: 1 }, { key: "a", value: 1 },
{ key: "b", value: 2 }, { key: "b", value: 2 },
{ key: "c", value: 3 } { key: "c", value: 3 },
]); ]);
}); });
@@ -51,7 +51,7 @@ describe("Core Utils", async () => {
const obj = utils.headersToObject(headers); const obj = utils.headersToObject(headers);
expect(obj).toEqual({ expect(obj).toEqual({
"content-type": "application/json", "content-type": "application/json",
authorization: "Bearer 123" authorization: "Bearer 123",
}); });
}); });
@@ -82,7 +82,7 @@ describe("Core Utils", async () => {
file: new File([""], "file.txt"), file: new File([""], "file.txt"),
stream: new ReadableStream(), stream: new ReadableStream(),
arrayBuffer: new ArrayBuffer(10), arrayBuffer: new ArrayBuffer(10),
arrayBufferView: new Uint8Array(new ArrayBuffer(10)) arrayBufferView: new Uint8Array(new ArrayBuffer(10)),
}; };
const fns = [ const fns = [
@@ -90,7 +90,7 @@ describe("Core Utils", async () => {
[utils.isBlob, "blob", ["stream", "arrayBuffer", "arrayBufferView"]], [utils.isBlob, "blob", ["stream", "arrayBuffer", "arrayBufferView"]],
[utils.isFile, "file", ["stream", "arrayBuffer", "arrayBufferView"]], [utils.isFile, "file", ["stream", "arrayBuffer", "arrayBufferView"]],
[utils.isArrayBuffer, "arrayBuffer"], [utils.isArrayBuffer, "arrayBuffer"],
[utils.isArrayBufferView, "arrayBufferView"] [utils.isArrayBufferView, "arrayBufferView"],
] as const; ] as const;
const additional = [0, 0.0, "", null, undefined, {}, []]; const additional = [0, 0.0, "", null, undefined, {}, []];
@@ -116,10 +116,10 @@ describe("Core Utils", async () => {
const name = "test.json"; const name = "test.json";
const text = "attachment; filename=" + name; const text = "attachment; filename=" + name;
const headers = new Headers({ const headers = new Headers({
"Content-Disposition": text "Content-Disposition": text,
}); });
const request = new Request("http://example.com", { const request = new Request("http://example.com", {
headers headers,
}); });
expect(utils.getContentName(text)).toBe(name); 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 }, ["b"], { a: 1, c: 3 }],
[{ a: 1, b: 2, c: 3 }, ["c"], { a: 1, b: 2 }], [{ a: 1, b: 2, c: 3 }, ["c"], { a: 1, b: 2 }],
[{ a: 1, b: 2, c: 3 }, ["a", "b"], { c: 3 }], [{ a: 1, b: 2, c: 3 }, ["a", "b"], { c: 3 }],
[{ a: 1, b: 2, c: 3 }, ["a", "b", "c"], {}] [{ a: 1, b: 2, c: 3 }, ["a", "b", "c"], {}],
] as [object, string[], object][]; ] as [object, string[], object][];
for (const [obj, keys, expected] of objects) { for (const [obj, keys, expected] of objects) {
@@ -197,9 +197,9 @@ describe("Core Utils", async () => {
new Map([["a", 1]]), new Map([["a", 1]]),
new Map([ new Map([
["a", 1], ["a", 1],
["b", 2] ["b", 2],
]), ]),
false false,
], ],
[{ a: 1 }, { a: 1 }, true], [{ a: 1 }, { a: 1 }, true],
[{ a: 1 }, { a: 2 }, false], [{ a: 1 }, { a: 2 }, false],
@@ -220,7 +220,7 @@ describe("Core Utils", async () => {
[[1, 2, 3], [1, 2, 3, 4], false], [[1, 2, 3], [1, 2, 3, 4], false],
[[{ a: 1 }], [{ a: 1 }], true], [[{ a: 1 }], [{ a: 1 }], true],
[[{ a: 1 }], [{ a: 2 }], false], [[{ a: 1 }], [{ a: 2 }], false],
[[{ a: 1 }], [{ b: 1 }], false] [[{ a: 1 }], [{ b: 1 }], false],
] as [any, any, boolean][]; ] as [any, any, boolean][];
for (const [a, b, expected] of objects) { for (const [a, b, expected] of objects) {
@@ -236,7 +236,7 @@ describe("Core Utils", async () => {
[{ a: { b: 1 } }, "a.b", 1], [{ a: { b: 1 } }, "a.b", 1],
[{ a: { b: 1 } }, "a.b.c", null, null], [{ a: { b: 1 } }, "a.b.c", null, null],
[{ a: { b: 1 } }, "a.b.c", 1, 1], [{ a: { b: 1 } }, "a.b.c", 1, 1],
[[[1]], "0.0", 1] [[[1]], "0.0", 1],
] as [object, string, any, any][]; ] as [object, string, any, any][];
for (const [obj, path, expected, defaultValue] of tests) { for (const [obj, path, expected, defaultValue] of tests) {
@@ -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);
});
});
}); });

View File

@@ -9,7 +9,7 @@ import {
ManyToOneRelation, ManyToOneRelation,
type MutatorResponse, type MutatorResponse,
type RepositoryResponse, type RepositoryResponse,
TextField TextField,
} from "../../src/data"; } from "../../src/data";
import { DataController } from "../../src/data/api/DataController"; import { DataController } from "../../src/data/api/DataController";
import { dataConfigSchema } from "../../src/data/data-schema"; import { dataConfigSchema } from "../../src/data/data-schema";
@@ -35,17 +35,17 @@ describe("[data] DataController", async () => {
meta: { meta: {
total: 0, total: 0,
count: 0, count: 0,
items: 0 items: 0,
} },
}); });
expect(res).toEqual({ expect(res).toEqual({
meta: { meta: {
total: 0, total: 0,
count: 0, count: 0,
items: 0 items: 0,
}, },
data: [] data: [],
}); });
}); });
@@ -59,22 +59,22 @@ describe("[data] DataController", async () => {
data: [] as any, data: [] as any,
sql: "", sql: "",
parameters: [] as any, parameters: [] as any,
result: [] as any result: [] as any,
}); });
expect(res).toEqual({ expect(res).toEqual({
data: [] data: [],
}); });
}); });
describe("getController", async () => { describe("getController", async () => {
const users = new Entity("users", [ const users = new Entity("users", [
new TextField("name", { required: true }), new TextField("name", { required: true }),
new TextField("bio") new TextField("bio"),
]); ]);
const posts = new Entity("posts", [new TextField("content")]); const posts = new Entity("posts", [new TextField("content")]);
const em = new EntityManager([users, posts], dummyConnection, [ const em = new EntityManager([users, posts], dummyConnection, [
new ManyToOneRelation(posts, users) new ManyToOneRelation(posts, users),
]); ]);
await em.schema().sync({ force: true }); await em.schema().sync({ force: true });
@@ -83,12 +83,12 @@ describe("[data] DataController", async () => {
users: [ users: [
{ name: "foo", bio: "bar" }, { name: "foo", bio: "bar" },
{ name: "bar", bio: null }, { name: "bar", bio: null },
{ name: "baz", bio: "!!!" } { name: "baz", bio: "!!!" },
], ],
posts: [ posts: [
{ content: "post 1", users_id: 1 }, { content: "post 1", users_id: 1 },
{ content: "post 2", users_id: 2 } { content: "post 2", users_id: 2 },
] ],
}; };
const ctx: any = { em, guard: new Guard() }; const ctx: any = { em, guard: new Guard() };
@@ -118,7 +118,7 @@ describe("[data] DataController", async () => {
for await (const _user of fixtures.users) { for await (const _user of fixtures.users) {
const res = await app.request("/entity/users", { const res = await app.request("/entity/users", {
method: "POST", method: "POST",
body: JSON.stringify(_user) body: JSON.stringify(_user),
}); });
//console.log("res", { _user }, res); //console.log("res", { _user }, res);
const result = (await res.json()) as MutatorResponse; const result = (await res.json()) as MutatorResponse;
@@ -133,7 +133,7 @@ describe("[data] DataController", async () => {
for await (const _post of fixtures.posts) { for await (const _post of fixtures.posts) {
const res = await app.request("/entity/posts", { const res = await app.request("/entity/posts", {
method: "POST", method: "POST",
body: JSON.stringify(_post) body: JSON.stringify(_post),
}); });
const result = (await res.json()) as MutatorResponse; const result = (await res.json()) as MutatorResponse;
const { id, ...data } = result.data as any; const { id, ...data } = result.data as any;
@@ -159,11 +159,11 @@ describe("[data] DataController", async () => {
const res = await app.request("/entity/users/query", { const res = await app.request("/entity/users/query", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
where: { bio: { $isnull: 1 } } where: { bio: { $isnull: 1 } },
}) }),
}); });
const data = (await res.json()) as RepositoryResponse; const data = (await res.json()) as RepositoryResponse;
@@ -199,7 +199,7 @@ describe("[data] DataController", async () => {
test("/:entity (update one)", async () => { test("/:entity (update one)", async () => {
const res = await app.request("/entity/users/3", { const res = await app.request("/entity/users/3", {
method: "PATCH", method: "PATCH",
body: JSON.stringify({ name: "new name" }) body: JSON.stringify({ name: "new name" }),
}); });
const { data } = (await res.json()) as MutatorResponse; const { data } = (await res.json()) as MutatorResponse;
@@ -221,7 +221,7 @@ describe("[data] DataController", async () => {
test("/:entity/:id (delete one)", async () => { test("/:entity/:id (delete one)", async () => {
const res = await app.request("/entity/posts/2", { const res = await app.request("/entity/posts/2", {
method: "DELETE" method: "DELETE",
}); });
const { data } = (await res.json()) as RepositoryResponse<EntityData>; const { data } = (await res.json()) as RepositoryResponse<EntityData>;
expect(data).toEqual({ id: 2, ...fixtures.posts[1] }); expect(data).toEqual({ id: 2, ...fixtures.posts[1] });

View File

@@ -30,7 +30,7 @@ describe("data-query-impl", () => {
[{ val: { $isnull: 0 } }, '"val" is not null', []], [{ val: { $isnull: 0 } }, '"val" is not null', []],
[{ val: { $isnull: false } }, '"val" is not null', []], [{ val: { $isnull: false } }, '"val" is not null', []],
[{ val: { $like: "what" } }, '"val" like ?', ["what"]], [{ val: { $like: "what" } }, '"val" like ?', ["what"]],
[{ val: { $like: "w*t" } }, '"val" like ?', ["w%t"]] [{ val: { $like: "w*t" } }, '"val" like ?', ["w%t"]],
]; ];
for (const [query, expectedSql, expectedParams] of tests) { for (const [query, expectedSql, expectedParams] of tests) {
@@ -51,22 +51,22 @@ describe("data-query-impl", () => {
[ [
{ val1: { $eq: "foo" }, val2: { $eq: "bar" } }, { val1: { $eq: "foo" }, val2: { $eq: "bar" } },
'("val1" = ? and "val2" = ?)', '("val1" = ? and "val2" = ?)',
["foo", "bar"] ["foo", "bar"],
], ],
[ [
{ val1: { $eq: "foo" }, val2: { $eq: "bar" } }, { val1: { $eq: "foo" }, val2: { $eq: "bar" } },
'("val1" = ? and "val2" = ?)', '("val1" = ? and "val2" = ?)',
["foo", "bar"] ["foo", "bar"],
], ],
// or constructs // or constructs
[ [
{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } }, { $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } },
'("val1" = ? or "val2" = ?)', '("val1" = ? or "val2" = ?)',
["foo", "bar"] ["foo", "bar"],
], ],
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, '("val1" = ? or "val1" = ?)', [1, 2]], [{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, '("val1" = ? or "val1" = ?)', [1, 2]],
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, '("val1" = ? or "val1" = ?)', [1, 2]] [{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, '("val1" = ? or "val1" = ?)', [1, 2]],
]; ];
for (const [query, expectedSql, expectedParams] of tests) { for (const [query, expectedSql, expectedParams] of tests) {
@@ -86,7 +86,7 @@ describe("data-query-impl", () => {
// or constructs // or constructs
[{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } }, ["val1", "val2"]], [{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } }, ["val1", "val2"]],
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, ["val1"]] [{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, ["val1"]],
]; ];
for (const [query, expectedKeys] of tests) { for (const [query, expectedKeys] of tests) {
@@ -105,23 +105,23 @@ describe("data-query-impl", () => {
posts: { posts: {
with: { with: {
images: { images: {
select: ["id"] select: ["id"],
} },
} },
} },
} },
}, },
{ {
with: { with: {
posts: { posts: {
with: { with: {
images: { images: {
select: ["id"] select: ["id"],
} },
} },
} },
} },
} },
); );
// over http // over http

View File

@@ -5,7 +5,7 @@ import {
NumberField, NumberField,
PrimaryField, PrimaryField,
Repository, Repository,
TextField TextField,
} from "../../src/data"; } from "../../src/data";
import { getDummyConnection } from "./helper"; import { getDummyConnection } from "./helper";
@@ -18,14 +18,14 @@ describe("some tests", async () => {
const users = new Entity("users", [ const users = new Entity("users", [
new TextField("username", { required: true, default_value: "nobody" }), new TextField("username", { required: true, default_value: "nobody" }),
new TextField("email", { maxLength: 3 }) new TextField("email", { maxLength: 3 }),
]); ]);
const posts = new Entity("posts", [ const posts = new Entity("posts", [
new TextField("title"), new TextField("title"),
new TextField("content"), new TextField("content"),
new TextField("created_at"), new TextField("created_at"),
new NumberField("likes", { default_value: 0 }) new NumberField("likes", { default_value: 0 }),
]); ]);
const em = new EntityManager([users, posts], connection); const em = new EntityManager([users, posts], connection);
@@ -43,7 +43,7 @@ describe("some tests", async () => {
});*/ });*/
expect(query.sql).toBe( expect(query.sql).toBe(
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" where "id" = ? limit ?' 'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" where "id" = ? limit ?',
); );
expect(query.parameters).toEqual([1, 1]); expect(query.parameters).toEqual([1, 1]);
expect(query.result).toEqual([]); expect(query.result).toEqual([]);
@@ -53,7 +53,7 @@ describe("some tests", async () => {
const query = await em.repository(users).findMany(); const query = await em.repository(users).findMany();
expect(query.sql).toBe( expect(query.sql).toBe(
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" order by "users"."id" asc limit ? offset ?' 'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" order by "users"."id" asc limit ? offset ?',
); );
expect(query.parameters).toEqual([10, 0]); expect(query.parameters).toEqual([10, 0]);
expect(query.result).toEqual([]); expect(query.result).toEqual([]);
@@ -63,7 +63,7 @@ describe("some tests", async () => {
const query = await em.repository(posts).findMany(); const query = await em.repository(posts).findMany();
expect(query.sql).toBe( expect(query.sql).toBe(
'select "posts"."id" as "id", "posts"."title" as "title", "posts"."content" as "content", "posts"."created_at" as "created_at", "posts"."likes" as "likes" from "posts" order by "posts"."id" asc limit ? offset ?' 'select "posts"."id" as "id", "posts"."title" as "title", "posts"."content" as "content", "posts"."created_at" as "created_at", "posts"."likes" as "likes" from "posts" order by "posts"."id" asc limit ? offset ?',
); );
expect(query.parameters).toEqual([10, 0]); expect(query.parameters).toEqual([10, 0]);
expect(query.result).toEqual([]); expect(query.result).toEqual([]);
@@ -74,7 +74,7 @@ describe("some tests", async () => {
new Entity("users", [ new Entity("users", [
new TextField("username"), new TextField("username"),
new TextField("email"), new TextField("email"),
new TextField("email") // not throwing, it's just being ignored new TextField("email"), // not throwing, it's just being ignored
]); ]);
}).toBeDefined(); }).toBeDefined();
@@ -83,7 +83,7 @@ describe("some tests", async () => {
new TextField("username"), new TextField("username"),
new TextField("email"), new TextField("email"),
// field config differs, will throw // field config differs, will throw
new TextField("email", { required: true }) new TextField("email", { required: true }),
]); ]);
}).toThrow(); }).toThrow();
@@ -91,7 +91,7 @@ describe("some tests", async () => {
new Entity("users", [ new Entity("users", [
new PrimaryField(), new PrimaryField(),
new TextField("username"), new TextField("username"),
new TextField("email") new TextField("email"),
]); ]);
}).toBeDefined(); }).toBeDefined();
}); });

View File

@@ -16,7 +16,7 @@ export function getDummyDatabase(memory: boolean = true): {
afterAllCleanup: async () => { afterAllCleanup: async () => {
if (!memory) await unlink(DB_NAME); if (!memory) await unlink(DB_NAME);
return true; return true;
} },
}; };
} }
@@ -26,7 +26,7 @@ export function getDummyConnection(memory: boolean = true) {
return { return {
dummyConnection, dummyConnection,
afterAllCleanup afterAllCleanup,
}; };
} }

View File

@@ -6,7 +6,7 @@ import {
ManyToOneRelation, ManyToOneRelation,
NumberField, NumberField,
SchemaManager, SchemaManager,
TextField TextField,
} from "../../src/data"; } from "../../src/data";
import { getDummyConnection } from "./helper"; import { getDummyConnection } from "./helper";
@@ -21,7 +21,7 @@ describe("Mutator relation", async () => {
const posts = new Entity("posts", [ const posts = new Entity("posts", [
new TextField("title"), new TextField("title"),
new TextField("content", { default_value: "..." }), new TextField("content", { default_value: "..." }),
new NumberField("count", { default_value: 0 }) new NumberField("count", { default_value: 0 }),
]); ]);
const users = new Entity("users", [new TextField("username")]); const users = new Entity("users", [new TextField("username")]);
@@ -44,7 +44,7 @@ describe("Mutator relation", async () => {
expect(em.mutator(posts).insertOne({ title: "post2", users_id: 10 })).rejects.toThrow(); expect(em.mutator(posts).insertOne({ title: "post2", users_id: 10 })).rejects.toThrow();
expect( expect(
em.mutator(posts).insertOne({ title: "post2", users_id: data.id }) em.mutator(posts).insertOne({ title: "post2", users_id: data.id }),
).resolves.toBeDefined(); ).resolves.toBeDefined();
}); });
}); });

View File

@@ -14,7 +14,7 @@ describe("Mutator simple", async () => {
const items = new Entity("items", [ const items = new Entity("items", [
new TextField("label", { required: true, minLength: 1 }), new TextField("label", { required: true, minLength: 1 }),
new NumberField("count", { default_value: 0 }) new NumberField("count", { default_value: 0 }),
]); ]);
const em = new EntityManager<any>([items], connection); const em = new EntityManager<any>([items], connection);
@@ -29,11 +29,11 @@ describe("Mutator simple", async () => {
test("insert single row", async () => { test("insert single row", async () => {
const mutation = await em.mutator(items).insertOne({ const mutation = await em.mutator(items).insertOne({
label: "test", label: "test",
count: 1 count: 1,
}); });
expect(mutation.sql).toBe( expect(mutation.sql).toBe(
'insert into "items" ("count", "label") values (?, ?) returning "id", "label", "count"' 'insert into "items" ("count", "label") values (?, ?) returning "id", "label", "count"',
); );
expect(mutation.data).toEqual({ id: 1, label: "test", count: 1 }); expect(mutation.data).toEqual({ id: 1, label: "test", count: 1 });
@@ -41,8 +41,8 @@ describe("Mutator simple", async () => {
limit: 1, limit: 1,
sort: { sort: {
by: "id", by: "id",
dir: "desc" dir: "desc",
} },
}); });
expect(query.result).toEqual([{ id: 1, label: "test", count: 1 }]); expect(query.result).toEqual([{ id: 1, label: "test", count: 1 }]);
@@ -53,18 +53,18 @@ describe("Mutator simple", async () => {
limit: 1, limit: 1,
sort: { sort: {
by: "id", by: "id",
dir: "desc" dir: "desc",
} },
}); });
const id = query.data![0].id as number; const id = query.data![0].id as number;
const mutation = await em.mutator(items).updateOne(id, { const mutation = await em.mutator(items).updateOne(id, {
label: "new label", label: "new label",
count: 100 count: 100,
}); });
expect(mutation.sql).toBe( expect(mutation.sql).toBe(
'update "items" set "label" = ?, "count" = ? where "id" = ? returning "id", "label", "count"' 'update "items" set "label" = ?, "count" = ? where "id" = ? returning "id", "label", "count"',
); );
expect(mutation.data).toEqual({ id, label: "new label", count: 100 }); expect(mutation.data).toEqual({ id, label: "new label", count: 100 });
}); });
@@ -74,15 +74,15 @@ describe("Mutator simple", async () => {
limit: 1, limit: 1,
sort: { sort: {
by: "id", by: "id",
dir: "desc" dir: "desc",
} },
}); });
const id = query.data![0].id as number; const id = query.data![0].id as number;
const mutation = await em.mutator(items).deleteOne(id); const mutation = await em.mutator(items).deleteOne(id);
expect(mutation.sql).toBe( expect(mutation.sql).toBe(
'delete from "items" where "id" = ? returning "id", "label", "count"' 'delete from "items" where "id" = ? returning "id", "label", "count"',
); );
expect(mutation.data).toEqual({ id, label: "new label", count: 100 }); expect(mutation.data).toEqual({ id, label: "new label", count: 100 });
@@ -94,7 +94,7 @@ describe("Mutator simple", async () => {
const incompleteCreate = async () => const incompleteCreate = async () =>
await em.mutator(items).insertOne({ await em.mutator(items).insertOne({
//label: "test", //label: "test",
count: 1 count: 1,
}); });
expect(incompleteCreate()).rejects.toThrow(); expect(incompleteCreate()).rejects.toThrow();
@@ -104,7 +104,7 @@ describe("Mutator simple", async () => {
const invalidCreate1 = async () => const invalidCreate1 = async () =>
await em.mutator(items).insertOne({ await em.mutator(items).insertOne({
label: 111, // this should work label: 111, // this should work
count: "1" // this should fail count: "1", // this should fail
}); });
expect(invalidCreate1()).rejects.toThrow(TransformPersistFailedException); expect(invalidCreate1()).rejects.toThrow(TransformPersistFailedException);
@@ -112,7 +112,7 @@ describe("Mutator simple", async () => {
const invalidCreate2 = async () => const invalidCreate2 = async () =>
await em.mutator(items).insertOne({ await em.mutator(items).insertOne({
label: "", // this should fail label: "", // this should fail
count: 1 count: 1,
}); });
expect(invalidCreate2()).rejects.toThrow(TransformPersistFailedException); expect(invalidCreate2()).rejects.toThrow(TransformPersistFailedException);
@@ -137,7 +137,7 @@ describe("Mutator simple", async () => {
expect((await em.repository(items).findMany()).data.length).toBe(data.length - 2); expect((await em.repository(items).findMany()).data.length).toBe(data.length - 2);
//console.log((await em.repository(items).findMany()).data); //console.log((await em.repository(items).findMany()).data);
await em.mutator(items).deleteWhere(); await em.mutator(items).deleteWhere({ id: { $isnull: 0 } });
expect((await em.repository(items).findMany()).data.length).toBe(0); expect((await em.repository(items).findMany()).data.length).toBe(0);
//expect(res.data.count).toBe(0); //expect(res.data.count).toBe(0);
@@ -152,27 +152,27 @@ describe("Mutator simple", async () => {
await em.mutator(items).updateWhere( await em.mutator(items).updateWhere(
{ count: 2 }, { count: 2 },
{ {
count: 10 count: 10,
} },
); );
expect((await em.repository(items).findMany()).data).toEqual([ expect((await em.repository(items).findMany()).data).toEqual([
{ id: 6, label: "update", count: 1 }, { id: 6, label: "update", count: 1 },
{ id: 7, label: "update too", count: 1 }, { id: 7, label: "update too", count: 1 },
{ id: 8, label: "keep", count: 0 } { id: 8, label: "keep", count: 0 },
]); ]);
// expect 2 to be updated // expect 2 to be updated
await em.mutator(items).updateWhere( await em.mutator(items).updateWhere(
{ count: 2 }, { count: 2 },
{ {
count: 1 count: 1,
} },
); );
expect((await em.repository(items).findMany()).data).toEqual([ expect((await em.repository(items).findMany()).data).toEqual([
{ id: 6, label: "update", count: 2 }, { id: 6, label: "update", count: 2 },
{ id: 7, label: "update too", count: 2 }, { id: 7, label: "update too", count: 2 },
{ id: 8, label: "keep", count: 0 } { id: 8, label: "keep", count: 0 },
]); ]);
}); });

View File

@@ -23,23 +23,23 @@ describe("Polymorphic", async () => {
source: "categories", source: "categories",
target: "media", target: "media",
config: { config: {
mappedBy: "image" mappedBy: "image",
} },
}); });
// media should not see categories // media should not see categories
expect(em.relationsOf(media.name).map((r) => r.toJSON())).toEqual([]); expect(em.relationsOf(media.name).map((r) => r.toJSON())).toEqual([]);
// it's important that media cannot access categories // it's important that media cannot access categories
expect(em.relations.targetRelationsOf(categories).map((r) => r.source.entity.name)).toEqual( expect(em.relations.targetRelationsOf(categories).map((r) => r.source.entity.name)).toEqual(
[] [],
); );
expect(em.relations.targetRelationsOf(media).map((r) => r.source.entity.name)).toEqual([]); expect(em.relations.targetRelationsOf(media).map((r) => r.source.entity.name)).toEqual([]);
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.entity.name)).toEqual([ expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.entity.name)).toEqual([
"media" "media",
]); ]);
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.reference)).toEqual([ expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.reference)).toEqual([
"image" "image",
]); ]);
expect(em.relations.sourceRelationsOf(media).map((r) => r.target.entity.name)).toEqual([]); expect(em.relations.sourceRelationsOf(media).map((r) => r.target.entity.name)).toEqual([]);
@@ -48,7 +48,7 @@ describe("Polymorphic", async () => {
"id", "id",
"path", "path",
"reference", "reference",
"entity_id" "entity_id",
]); ]);
expect(media.getSelect()).toEqual(["id", "path"]); expect(media.getSelect()).toEqual(["id", "path"]);
}); });
@@ -60,7 +60,7 @@ describe("Polymorphic", async () => {
const entities = [media, categories]; const entities = [media, categories];
const single = new PolymorphicRelation(categories, media, { const single = new PolymorphicRelation(categories, media, {
mappedBy: "single", mappedBy: "single",
targetCardinality: 1 targetCardinality: 1,
}); });
const multiple = new PolymorphicRelation(categories, media, { mappedBy: "multiple" }); const multiple = new PolymorphicRelation(categories, media, { mappedBy: "multiple" });
@@ -71,17 +71,17 @@ describe("Polymorphic", async () => {
// it's important that media cannot access categories // it's important that media cannot access categories
expect(em.relations.targetRelationsOf(categories).map((r) => r.source.entity.name)).toEqual( expect(em.relations.targetRelationsOf(categories).map((r) => r.source.entity.name)).toEqual(
[] [],
); );
expect(em.relations.targetRelationsOf(media).map((r) => r.source.entity.name)).toEqual([]); expect(em.relations.targetRelationsOf(media).map((r) => r.source.entity.name)).toEqual([]);
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.entity.name)).toEqual([ expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.entity.name)).toEqual([
"media", "media",
"media" "media",
]); ]);
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.reference)).toEqual([ expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.reference)).toEqual([
"single", "single",
"multiple" "multiple",
]); ]);
expect(em.relations.sourceRelationsOf(media).map((r) => r.target.entity.name)).toEqual([]); expect(em.relations.sourceRelationsOf(media).map((r) => r.target.entity.name)).toEqual([]);
@@ -90,7 +90,7 @@ describe("Polymorphic", async () => {
"id", "id",
"path", "path",
"reference", "reference",
"entity_id" "entity_id",
]); ]);
}); });
}); });

View File

@@ -12,7 +12,7 @@ import {
NumberField, NumberField,
OneToOneRelation, OneToOneRelation,
PolymorphicRelation, PolymorphicRelation,
TextField TextField,
} from "../../src/data"; } from "../../src/data";
import { DummyConnection } from "../../src/data/connection/DummyConnection"; import { DummyConnection } from "../../src/data/connection/DummyConnection";
import { import {
@@ -31,7 +31,7 @@ import {
medium, medium,
number, number,
relation, relation,
text text,
} from "../../src/data/prototype"; } from "../../src/data/prototype";
import { MediaField } from "../../src/media/MediaField"; import { MediaField } from "../../src/media/MediaField";
@@ -54,7 +54,7 @@ describe("prototype", () => {
name: text(), name: text(),
bio: text(), bio: text(),
age: number(), age: number(),
some: number() some: number(),
}); });
type db = { type db = {
users: Schema<typeof users>; users: Schema<typeof users>;
@@ -70,7 +70,7 @@ describe("prototype", () => {
name: text({ default_value: "hello" }).required(), name: text({ default_value: "hello" }).required(),
bio: text(), bio: text(),
age: number(), age: number(),
some: number().required() some: number().required(),
}); });
const obj: InsertSchema<typeof user> = { name: "yo", some: 1 }; const obj: InsertSchema<typeof user> = { name: "yo", some: 1 };
@@ -83,12 +83,12 @@ describe("prototype", () => {
new TextField("title", { required: true }), new TextField("title", { required: true }),
new TextField("content"), new TextField("content"),
new DateField("created_at", { new DateField("created_at", {
type: "datetime" type: "datetime",
}), }),
// @ts-ignore // @ts-ignore
new MediaField("images", { entity: "posts" }), new MediaField("images", { entity: "posts" }),
// @ts-ignore // @ts-ignore
new MediaField("cover", { entity: "posts", max_items: 1 }) new MediaField("cover", { entity: "posts", max_items: 1 }),
]); ]);
const posts2 = entity("posts", { const posts2 = entity("posts", {
@@ -96,7 +96,7 @@ describe("prototype", () => {
content: text(), content: text(),
created_at: datetime(), created_at: datetime(),
images: media(), images: media(),
cover: medium() cover: medium(),
}); });
type Posts = Schema<typeof posts2>; type Posts = Schema<typeof posts2>;
@@ -117,11 +117,11 @@ describe("prototype", () => {
type: "objects", type: "objects",
values: [ values: [
{ value: "active", label: "Active" }, { value: "active", label: "Active" },
{ value: "inactive", label: "Not active" } { value: "inactive", label: "Not active" },
] ],
} },
}), }),
new JsonField("json") new JsonField("json"),
]); ]);
const test2 = entity("test", { const test2 = entity("test", {
@@ -134,10 +134,10 @@ describe("prototype", () => {
status: enumm<"active" | "inactive">({ status: enumm<"active" | "inactive">({
enum: [ enum: [
{ value: "active", label: "Active" }, { value: "active", label: "Active" },
{ value: "inactive", label: "Not active" } { value: "inactive", label: "Not active" },
] ],
}), }),
json: json<{ some: number }>() json: json<{ some: number }>(),
}); });
expect(test.toJSON()).toEqual(test2.toJSON()); expect(test.toJSON()).toEqual(test2.toJSON());
@@ -161,12 +161,12 @@ describe("prototype", () => {
// category has single image // category has single image
new PolymorphicRelation(categories, _media, { new PolymorphicRelation(categories, _media, {
mappedBy: "image", mappedBy: "image",
targetCardinality: 1 targetCardinality: 1,
}), }),
// post has multiple images // post has multiple images
new PolymorphicRelation(posts, _media, { mappedBy: "images" }), new PolymorphicRelation(posts, _media, { mappedBy: "images" }),
new PolymorphicRelation(posts, _media, { mappedBy: "cover", targetCardinality: 1 }) new PolymorphicRelation(posts, _media, { mappedBy: "cover", targetCardinality: 1 }),
]; ];
const relations2 = [ const relations2 = [
@@ -180,7 +180,7 @@ describe("prototype", () => {
relation(categories).polyToOne(_media, { mappedBy: "image" }), relation(categories).polyToOne(_media, { mappedBy: "image" }),
relation(posts).polyToMany(_media, { mappedBy: "images" }), relation(posts).polyToMany(_media, { mappedBy: "images" }),
relation(posts).polyToOne(_media, { mappedBy: "cover" }) relation(posts).polyToOne(_media, { mappedBy: "cover" }),
]; ];
expect(relations.map((r) => r.toJSON())).toEqual(relations2.map((r) => r.toJSON())); expect(relations.map((r) => r.toJSON())).toEqual(relations2.map((r) => r.toJSON()));
@@ -194,21 +194,21 @@ describe("prototype", () => {
posts, posts,
categories, categories,
{ {
connectionTableMappedName: "custom" connectionTableMappedName: "custom",
}, },
[new TextField("description")] [new TextField("description")],
); );
const fields = { const fields = {
description: text() description: text(),
}; };
let o: FieldSchema<typeof fields>; let o: FieldSchema<typeof fields>;
const rel2 = relation(posts).manyToMany( const rel2 = relation(posts).manyToMany(
categories, categories,
{ {
connectionTableMappedName: "custom" connectionTableMappedName: "custom",
}, },
fields fields,
); );
expect(rel.toJSON()).toEqual(rel2.toJSON()); expect(rel.toJSON()).toEqual(rel2.toJSON());
@@ -216,11 +216,11 @@ describe("prototype", () => {
test("devexample", async () => { test("devexample", async () => {
const users = entity("users", { const users = entity("users", {
username: text() username: text(),
}); });
const comments = entity("comments", { const comments = entity("comments", {
content: text() content: text(),
}); });
const posts = entity("posts", { const posts = entity("posts", {
@@ -228,17 +228,17 @@ describe("prototype", () => {
content: text(), content: text(),
created_at: datetime(), created_at: datetime(),
images: media(), images: media(),
cover: medium() cover: medium(),
}); });
const categories = entity("categories", { const categories = entity("categories", {
name: text(), name: text(),
description: text(), description: text(),
image: medium() image: medium(),
}); });
const settings = entity("settings", { const settings = entity("settings", {
theme: text() theme: text(),
}); });
const test = entity("test", { const test = entity("test", {
@@ -251,10 +251,10 @@ describe("prototype", () => {
status: enumm<"active" | "inactive">({ status: enumm<"active" | "inactive">({
enum: [ enum: [
{ value: "active", label: "Active" }, { value: "active", label: "Active" },
{ value: "inactive", label: "Not active" } { value: "inactive", label: "Not active" },
] ],
}), }),
json: json<{ some: number }>() json: json<{ some: number }>(),
}); });
const _media = entity("media", {}); const _media = entity("media", {});
@@ -270,7 +270,7 @@ describe("prototype", () => {
relation(users).oneToOne(settings), relation(users).oneToOne(settings),
relation(comments).manyToOne(users, { required: true }), relation(comments).manyToOne(users, { required: true }),
relation(comments).manyToOne(posts, { required: true }) relation(comments).manyToOne(posts, { required: true }),
]; ];
const obj: Schema<typeof test> = {} as any; const obj: Schema<typeof test> = {} as any;
@@ -281,12 +281,12 @@ describe("prototype", () => {
{ {
posts: entity("posts", { name: text(), slug: text().required() }), posts: entity("posts", { name: text(), slug: text().required() }),
comments: entity("comments", { some: text() }), comments: entity("comments", { some: text() }),
users: entity("users", { email: text() }) users: entity("users", { email: text() }),
}, },
({ relation, index }, { posts, comments, users }) => { ({ relation, index }, { posts, comments, users }) => {
relation(posts).manyToOne(comments).manyToOne(users); relation(posts).manyToOne(comments).manyToOne(users);
index(posts).on(["name"]).on(["slug"], true); index(posts).on(["name"]).on(["slug"], true);
} },
); );
type LocalDb = (typeof _em)["DB"]; type LocalDb = (typeof _em)["DB"];
@@ -294,7 +294,7 @@ describe("prototype", () => {
const es = [ const es = [
new Entity("posts", [new TextField("name"), new TextField("slug", { required: true })]), new Entity("posts", [new TextField("name"), new TextField("slug", { required: true })]),
new Entity("comments", [new TextField("some")]), new Entity("comments", [new TextField("some")]),
new Entity("users", [new TextField("email")]) new Entity("users", [new TextField("email")]),
]; ];
const _em2 = new EntityManager( const _em2 = new EntityManager(
es, es,
@@ -302,8 +302,8 @@ describe("prototype", () => {
[new ManyToOneRelation(es[0], es[1]), new ManyToOneRelation(es[0], es[2])], [new ManyToOneRelation(es[0], es[1]), new ManyToOneRelation(es[0], es[2])],
[ [
new EntityIndex(es[0], [es[0].field("name")!]), new EntityIndex(es[0], [es[0].field("name")!]),
new EntityIndex(es[0], [es[0].field("slug")!], true) new EntityIndex(es[0], [es[0].field("slug")!], true),
] ],
); );
// @ts-ignore // @ts-ignore

View File

@@ -6,7 +6,7 @@ import {
ManyToOneRelation, ManyToOneRelation,
OneToOneRelation, OneToOneRelation,
PolymorphicRelation, PolymorphicRelation,
RelationField RelationField,
} from "../../src/data/relations"; } from "../../src/data/relations";
import { getDummyConnection } from "./helper"; import { getDummyConnection } from "./helper";
@@ -22,7 +22,7 @@ describe("Relations", async () => {
const r1 = new RelationField("users_id", { const r1 = new RelationField("users_id", {
reference: "users", reference: "users",
target: "users", target: "users",
target_field: "id" target_field: "id",
}); });
const sql1 = schema const sql1 = schema
@@ -31,14 +31,14 @@ describe("Relations", async () => {
.compile().sql; .compile().sql;
expect(sql1).toBe( expect(sql1).toBe(
'create table "posts" ("users_id" integer references "users" ("id") on delete set null)' 'create table "posts" ("users_id" integer references "users" ("id") on delete set null)',
); );
//const r2 = new RelationField(new Entity("users"), "author"); //const r2 = new RelationField(new Entity("users"), "author");
const r2 = new RelationField("author_id", { const r2 = new RelationField("author_id", {
reference: "author", reference: "author",
target: "users", target: "users",
target_field: "id" target_field: "id",
}); });
const sql2 = schema const sql2 = schema
@@ -47,7 +47,7 @@ describe("Relations", async () => {
.compile().sql; .compile().sql;
expect(sql2).toBe( expect(sql2).toBe(
'create table "posts" ("author_id" integer references "users" ("id") on delete set null)' 'create table "posts" ("author_id" integer references "users" ("id") on delete set null)',
); );
}); });
@@ -57,7 +57,7 @@ describe("Relations", async () => {
reference: "users", reference: "users",
target: "users", target: "users",
target_field: "id", target_field: "id",
required: true required: true,
}); });
expect(r1.isRequired()).toBeTrue(); expect(r1.isRequired()).toBeTrue();
}); });
@@ -66,8 +66,8 @@ describe("Relations", async () => {
const users = new Entity("users", [new TextField("username")]); const users = new Entity("users", [new TextField("username")]);
const posts = new Entity("posts", [ const posts = new Entity("posts", [
new TextField("title", { new TextField("title", {
maxLength: 2 maxLength: 2,
}) }),
]); ]);
const entities = [users, posts]; const entities = [users, posts];
@@ -122,7 +122,7 @@ describe("Relations", async () => {
.selectFrom(users.name) .selectFrom(users.name)
.select((eb) => postAuthorRel.buildWith(users, "posts")(eb).as("posts")); .select((eb) => postAuthorRel.buildWith(users, "posts")(eb).as("posts"));
expect(selectPostsFromUsers.compile().sql).toBe( expect(selectPostsFromUsers.compile().sql).toBe(
'select (select from "posts" as "posts" where "posts"."author_id" = "users"."id") as "posts" from "users"' 'select (select from "posts" as "posts" where "posts"."author_id" = "users"."id") as "posts" from "users"',
); );
expect(postAuthorRel!.getField()).toBeInstanceOf(RelationField); expect(postAuthorRel!.getField()).toBeInstanceOf(RelationField);
const userObj = { id: 1, username: "test" }; const userObj = { id: 1, username: "test" };
@@ -142,7 +142,7 @@ describe("Relations", async () => {
.select((eb) => postAuthorRel.buildWith(posts, "author")(eb).as("author")); .select((eb) => postAuthorRel.buildWith(posts, "author")(eb).as("author"));
expect(selectUsersFromPosts.compile().sql).toBe( expect(selectUsersFromPosts.compile().sql).toBe(
'select (select from "users" as "author" where "author"."id" = "posts"."author_id" limit ?) as "author" from "posts"' 'select (select from "users" as "author" where "author"."id" = "posts"."author_id" limit ?) as "author" from "posts"',
); );
expect(postAuthorRel.getField()).toBeInstanceOf(RelationField); expect(postAuthorRel.getField()).toBeInstanceOf(RelationField);
const postObj = { id: 1, title: "test" }; const postObj = { id: 1, title: "test" };
@@ -158,7 +158,7 @@ describe("Relations", async () => {
$detach: false, $detach: false,
primary: undefined, primary: undefined,
cardinality: undefined, cardinality: undefined,
relation_type: "n:1" relation_type: "n:1",
}); });
expect(postAuthorRel!.helper(posts.name)!.getMutationInfo()).toEqual({ expect(postAuthorRel!.helper(posts.name)!.getMutationInfo()).toEqual({
@@ -170,7 +170,7 @@ describe("Relations", async () => {
$detach: false, $detach: false,
primary: "id", primary: "id",
cardinality: 1, cardinality: 1,
relation_type: "n:1" relation_type: "n:1",
}); });
/*console.log("ManyToOne (source=posts, target=users)"); /*console.log("ManyToOne (source=posts, target=users)");
@@ -225,7 +225,7 @@ describe("Relations", async () => {
$detach: false, $detach: false,
primary: "id", primary: "id",
cardinality: 1, cardinality: 1,
relation_type: "1:1" relation_type: "1:1",
}); });
expect(userSettingRel!.helper(settings.name)!.getMutationInfo()).toEqual({ expect(userSettingRel!.helper(settings.name)!.getMutationInfo()).toEqual({
reference: "users", reference: "users",
@@ -236,7 +236,7 @@ describe("Relations", async () => {
$detach: false, $detach: false,
primary: undefined, primary: undefined,
cardinality: 1, cardinality: 1,
relation_type: "1:1" relation_type: "1:1",
}); });
/*console.log(""); /*console.log("");
@@ -310,16 +310,16 @@ describe("Relations", async () => {
*/ */
const selectCategoriesFromPosts = kysely const selectCategoriesFromPosts = kysely
.selectFrom(posts.name) .selectFrom(posts.name)
.select((eb) => postCategoriesRel.buildWith(posts)(eb).as("categories")); .select((eb) => postCategoriesRel.buildWith(posts)(eb).select("id").as("categories"));
expect(selectCategoriesFromPosts.compile().sql).toBe( expect(selectCategoriesFromPosts.compile().sql).toBe(
'select (select "categories"."id" as "id", "categories"."label" as "label" from "categories" inner join "posts_categories" on "categories"."id" = "posts_categories"."categories_id" where "posts"."id" = "posts_categories"."posts_id" limit ?) as "categories" from "posts"' 'select (select "id" from "categories" inner join "posts_categories" on "categories"."id" = "posts_categories"."categories_id" where "posts"."id" = "posts_categories"."posts_id" limit ?) as "categories" from "posts"',
); );
const selectPostsFromCategories = kysely const selectPostsFromCategories = kysely
.selectFrom(categories.name) .selectFrom(categories.name)
.select((eb) => postCategoriesRel.buildWith(categories)(eb).as("posts")); .select((eb) => postCategoriesRel.buildWith(categories)(eb).select("id").as("posts"));
expect(selectPostsFromCategories.compile().sql).toBe( expect(selectPostsFromCategories.compile().sql).toBe(
'select (select "posts"."id" as "id", "posts"."title" as "title" from "posts" inner join "posts_categories" on "posts"."id" = "posts_categories"."posts_id" where "categories"."id" = "posts_categories"."categories_id" limit ?) as "posts" from "categories"' 'select (select "id" from "posts" inner join "posts_categories" on "posts"."id" = "posts_categories"."posts_id" where "categories"."id" = "posts_categories"."categories_id" limit ?) as "posts" from "categories"',
); );
// mutation info // mutation info
@@ -332,7 +332,7 @@ describe("Relations", async () => {
$detach: true, $detach: true,
primary: "id", primary: "id",
cardinality: undefined, cardinality: undefined,
relation_type: "m:n" relation_type: "m:n",
}); });
expect(relations[0].helper(categories.name)!.getMutationInfo()).toEqual({ expect(relations[0].helper(categories.name)!.getMutationInfo()).toEqual({
reference: "posts", reference: "posts",
@@ -343,7 +343,7 @@ describe("Relations", async () => {
$detach: false, $detach: false,
primary: undefined, primary: undefined,
cardinality: undefined, cardinality: undefined,
relation_type: "m:n" relation_type: "m:n",
}); });
/*console.log(""); /*console.log("");

View File

@@ -6,7 +6,7 @@ describe("[data] Entity", async () => {
new TextField("name", { required: true }), new TextField("name", { required: true }),
new TextField("description"), new TextField("description"),
new NumberField("age", { fillable: false, default_value: 18 }), new NumberField("age", { fillable: false, default_value: 18 }),
new TextField("hidden", { hidden: true, default_value: "secret" }) new TextField("hidden", { hidden: true, default_value: "secret" }),
]); ]);
test("getSelect", async () => { test("getSelect", async () => {
@@ -17,7 +17,7 @@ describe("[data] Entity", async () => {
expect(entity.getFillableFields().map((f) => f.name)).toEqual([ expect(entity.getFillableFields().map((f) => f.name)).toEqual([
"name", "name",
"description", "description",
"hidden" "hidden",
]); ]);
}); });
@@ -28,7 +28,7 @@ describe("[data] Entity", async () => {
test("getDefaultObject", async () => { test("getDefaultObject", async () => {
expect(entity.getDefaultObject()).toEqual({ expect(entity.getDefaultObject()).toEqual({
age: 18, age: 18,
hidden: "secret" hidden: "secret",
}); });
}); });

View File

@@ -4,7 +4,7 @@ import {
EntityManager, EntityManager,
ManyToManyRelation, ManyToManyRelation,
ManyToOneRelation, ManyToOneRelation,
SchemaManager SchemaManager,
} from "../../../src/data"; } from "../../../src/data";
import { UnableToConnectException } from "../../../src/data/errors"; import { UnableToConnectException } from "../../../src/data/errors";
import { getDummyConnection } from "../helper"; import { getDummyConnection } from "../helper";
@@ -25,7 +25,7 @@ describe("[data] EntityManager", async () => {
expect(await em.ping()).toBe(true); expect(await em.ping()).toBe(true);
expect(() => em.entity("...")).toThrow(); expect(() => em.entity("...")).toThrow();
expect(() => expect(() =>
em.addRelation(new ManyToOneRelation(new Entity("1"), new Entity("2"))) em.addRelation(new ManyToOneRelation(new Entity("1"), new Entity("2"))),
).toThrow(); ).toThrow();
expect(em.schema()).toBeInstanceOf(SchemaManager); expect(em.schema()).toBeInstanceOf(SchemaManager);
@@ -98,7 +98,7 @@ describe("[data] EntityManager", async () => {
expect(userTargetRel.map((r) => r.other(users).entity.name)).toEqual(["posts", "comments"]); expect(userTargetRel.map((r) => r.other(users).entity.name)).toEqual(["posts", "comments"]);
expect(postTargetRel.map((r) => r.other(posts).entity.name)).toEqual([ expect(postTargetRel.map((r) => r.other(posts).entity.name)).toEqual([
"comments", "comments",
"categories" "categories",
]); ]);
expect(commentTargetRel.map((r) => r.other(comments).entity.name)).toEqual([]); expect(commentTargetRel.map((r) => r.other(comments).entity.name)).toEqual([]);
expect(categoriesTargetRel.map((r) => r.other(categories).entity.name)).toEqual(["posts"]); expect(categoriesTargetRel.map((r) => r.other(categories).entity.name)).toEqual(["posts"]);

View File

@@ -12,7 +12,7 @@ describe("[data] JoinBuilder", async () => {
const em = new EntityManager([users], dummyConnection); const em = new EntityManager([users], dummyConnection);
expect(() => expect(() =>
JoinBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, ["posts"]) JoinBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, ["posts"]),
).toThrow('Relation "posts" not found'); ).toThrow('Relation "posts" not found');
}); });
@@ -23,7 +23,7 @@ describe("[data] JoinBuilder", async () => {
const em = new EntityManager([users, posts], dummyConnection, relations); const em = new EntityManager([users, posts], dummyConnection, relations);
const qb = JoinBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, [ const qb = JoinBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, [
"posts" "posts",
]); ]);
const res = qb.compile(); const res = qb.compile();
@@ -34,7 +34,7 @@ describe("[data] JoinBuilder", async () => {
);*/ );*/
const qb2 = JoinBuilder.addClause(em, em.connection.kysely.selectFrom("posts"), posts, [ const qb2 = JoinBuilder.addClause(em, em.connection.kysely.selectFrom("posts"), posts, [
"author" "author",
]); ]);
const res2 = qb2.compile(); const res2 = qb2.compile();

View File

@@ -9,7 +9,7 @@ import {
OneToOneRelation, OneToOneRelation,
type RelationField, type RelationField,
RelationMutator, RelationMutator,
TextField TextField,
} from "../../../src/data"; } from "../../../src/data";
import * as proto from "../../../src/data/prototype"; import * as proto from "../../../src/data/prototype";
import { getDummyConnection } from "../helper"; import { getDummyConnection } from "../helper";
@@ -22,7 +22,7 @@ describe("[data] Mutator (base)", async () => {
new TextField("label", { required: true }), new TextField("label", { required: true }),
new NumberField("count"), new NumberField("count"),
new TextField("hidden", { hidden: true }), new TextField("hidden", { hidden: true }),
new TextField("not_fillable", { fillable: false }) new TextField("not_fillable", { fillable: false }),
]); ]);
const em = new EntityManager<any>([entity], dummyConnection); const em = new EntityManager<any>([entity], dummyConnection);
await em.schema().sync({ force: true }); await em.schema().sync({ force: true });
@@ -44,7 +44,7 @@ describe("[data] Mutator (base)", async () => {
test("updateOne", async () => { test("updateOne", async () => {
const { data } = await em.mutator(entity).insertOne(payload); const { data } = await em.mutator(entity).insertOne(payload);
const updated = await em.mutator(entity).updateOne(data.id, { const updated = await em.mutator(entity).updateOne(data.id, {
count: 2 count: 2,
}); });
expect(updated.parameters).toEqual([2, data.id]); expect(updated.parameters).toEqual([2, data.id]);
@@ -77,7 +77,7 @@ describe("[data] Mutator (ManyToOne)", async () => {
// persisting relational field should just return key value to be added // persisting relational field should just return key value to be added
expect( expect(
postRelMutator.persistRelationField(postRelField, "users_id", userData.data.id) postRelMutator.persistRelationField(postRelField, "users_id", userData.data.id),
).resolves.toEqual(["users_id", userData.data.id]); ).resolves.toEqual(["users_id", userData.data.id]);
// persisting invalid value should throw // persisting invalid value should throw
@@ -86,8 +86,8 @@ describe("[data] Mutator (ManyToOne)", async () => {
// persisting reference should ... // persisting reference should ...
expect( expect(
postRelMutator.persistReference(relations[0]!, "users", { postRelMutator.persistReference(relations[0]!, "users", {
$set: { id: userData.data.id } $set: { id: userData.data.id },
}) }),
).resolves.toEqual(["users_id", userData.data.id]); ).resolves.toEqual(["users_id", userData.data.id]);
// @todo: add what methods are allowed to relation, like $create should not be allowed for post<>users // @todo: add what methods are allowed to relation, like $create should not be allowed for post<>users
@@ -99,8 +99,8 @@ describe("[data] Mutator (ManyToOne)", async () => {
expect( expect(
em.mutator(posts).insertOne({ em.mutator(posts).insertOne({
title: "post1", title: "post1",
users_id: 100 // user does not exist yet users_id: 100, // user does not exist yet
}) }),
).rejects.toThrow(); ).rejects.toThrow();
}); });
@@ -111,7 +111,7 @@ describe("[data] Mutator (ManyToOne)", async () => {
const em = new EntityManager([items, cats], dummyConnection, relations); const em = new EntityManager([items, cats], dummyConnection, relations);
expect(em.mutator(items).insertOne({ label: "test" })).rejects.toThrow( expect(em.mutator(items).insertOne({ label: "test" })).rejects.toThrow(
'Field "cats_id" is required' 'Field "cats_id" is required',
); );
}); });
@@ -119,14 +119,14 @@ describe("[data] Mutator (ManyToOne)", async () => {
const { data } = await em.mutator(users).insertOne({ username: "user1" }); const { data } = await em.mutator(users).insertOne({ username: "user1" });
const res = await em.mutator(posts).insertOne({ const res = await em.mutator(posts).insertOne({
title: "post1", title: "post1",
users_id: data.id users_id: data.id,
}); });
expect(res.data.users_id).toBe(data.id); expect(res.data.users_id).toBe(data.id);
// setting "null" should be allowed // setting "null" should be allowed
const res2 = await em.mutator(posts).insertOne({ const res2 = await em.mutator(posts).insertOne({
title: "post1", title: "post1",
users_id: null users_id: null,
}); });
expect(res2.data.users_id).toBe(null); expect(res2.data.users_id).toBe(null);
}); });
@@ -135,14 +135,14 @@ describe("[data] Mutator (ManyToOne)", async () => {
const { data } = await em.mutator(users).insertOne({ username: "user1" }); const { data } = await em.mutator(users).insertOne({ username: "user1" });
const res = await em.mutator(posts).insertOne({ const res = await em.mutator(posts).insertOne({
title: "post1", title: "post1",
users: { $set: { id: data.id } } users: { $set: { id: data.id } },
}); });
expect(res.data.users_id).toBe(data.id); expect(res.data.users_id).toBe(data.id);
// setting "null" should be allowed // setting "null" should be allowed
const res2 = await em.mutator(posts).insertOne({ const res2 = await em.mutator(posts).insertOne({
title: "post1", title: "post1",
users: { $set: { id: null } } users: { $set: { id: null } },
}); });
expect(res2.data.users_id).toBe(null); expect(res2.data.users_id).toBe(null);
}); });
@@ -151,8 +151,8 @@ describe("[data] Mutator (ManyToOne)", async () => {
expect( expect(
em.mutator(posts).insertOne({ em.mutator(posts).insertOne({
title: "test", title: "test",
users: { $create: { username: "test" } } users: { $create: { username: "test" } },
}) }),
).rejects.toThrow(); ).rejects.toThrow();
}); });
@@ -162,27 +162,27 @@ describe("[data] Mutator (ManyToOne)", async () => {
const res2 = await em.mutator(posts).insertOne({ title: "post1" }); const res2 = await em.mutator(posts).insertOne({ title: "post1" });
const up1 = await em.mutator(posts).updateOne(res2.data.id, { const up1 = await em.mutator(posts).updateOne(res2.data.id, {
users: { $set: { id: res1.data.id } } users: { $set: { id: res1.data.id } },
}); });
expect(up1.data.users_id).toBe(res1.data.id); expect(up1.data.users_id).toBe(res1.data.id);
const up2 = await em.mutator(posts).updateOne(res2.data.id, { const up2 = await em.mutator(posts).updateOne(res2.data.id, {
users: { $set: { id: res1_1.data.id } } users: { $set: { id: res1_1.data.id } },
}); });
expect(up2.data.users_id).toBe(res1_1.data.id); expect(up2.data.users_id).toBe(res1_1.data.id);
const up3_1 = await em.mutator(posts).updateOne(res2.data.id, { const up3_1 = await em.mutator(posts).updateOne(res2.data.id, {
users_id: res1.data.id users_id: res1.data.id,
}); });
expect(up3_1.data.users_id).toBe(res1.data.id); expect(up3_1.data.users_id).toBe(res1.data.id);
const up3_2 = await em.mutator(posts).updateOne(res2.data.id, { const up3_2 = await em.mutator(posts).updateOne(res2.data.id, {
users_id: res1_1.data.id users_id: res1_1.data.id,
}); });
expect(up3_2.data.users_id).toBe(res1_1.data.id); expect(up3_2.data.users_id).toBe(res1_1.data.id);
const up4 = await em.mutator(posts).updateOne(res2.data.id, { const up4 = await em.mutator(posts).updateOne(res2.data.id, {
users_id: null users_id: null,
}); });
expect(up4.data.users_id).toBe(null); expect(up4.data.users_id).toBe(null);
}); });
@@ -199,8 +199,8 @@ describe("[data] Mutator (OneToOne)", async () => {
expect( expect(
em.mutator(users).insertOne({ em.mutator(users).insertOne({
username: "test", username: "test",
settings_id: 1 // todo: throws because it doesn't exist, but it shouldn't be allowed settings_id: 1, // todo: throws because it doesn't exist, but it shouldn't be allowed
}) }),
).rejects.toThrow(); ).rejects.toThrow();
}); });
@@ -210,15 +210,15 @@ describe("[data] Mutator (OneToOne)", async () => {
expect( expect(
em.mutator(users).insertOne({ em.mutator(users).insertOne({
username: "test", username: "test",
settings: { $set: { id: data.id } } settings: { $set: { id: data.id } },
}) }),
).rejects.toThrow(); ).rejects.toThrow();
}); });
test("insertOne: using $create", async () => { test("insertOne: using $create", async () => {
const res = await em.mutator(users).insertOne({ const res = await em.mutator(users).insertOne({
username: "test", username: "test",
settings: { $create: { theme: "dark" } } settings: { $create: { theme: "dark" } },
}); });
expect(res.data.settings_id).toBeDefined(); expect(res.data.settings_id).toBeDefined();
}); });
@@ -303,7 +303,7 @@ describe("[data] Mutator (Events)", async () => {
test("insertOne event return is respected", async () => { test("insertOne event return is respected", async () => {
const posts = proto.entity("posts", { const posts = proto.entity("posts", {
title: proto.text(), title: proto.text(),
views: proto.number() views: proto.number(),
}); });
const conn = getDummyConnection(); const conn = getDummyConnection();
@@ -318,10 +318,10 @@ describe("[data] Mutator (Events)", async () => {
async (event) => { async (event) => {
return { return {
...event.params.data, ...event.params.data,
views: 2 views: 2,
}; };
}, },
"sync" "sync",
); );
const mutator = em.mutator("posts"); const mutator = em.mutator("posts");
@@ -329,14 +329,14 @@ describe("[data] Mutator (Events)", async () => {
expect(result.data).toEqual({ expect(result.data).toEqual({
id: 1, id: 1,
title: "test", title: "test",
views: 2 views: 2,
}); });
}); });
test("updateOne event return is respected", async () => { test("updateOne event return is respected", async () => {
const posts = proto.entity("posts", { const posts = proto.entity("posts", {
title: proto.text(), title: proto.text(),
views: proto.number() views: proto.number(),
}); });
const conn = getDummyConnection(); const conn = getDummyConnection();
@@ -351,10 +351,10 @@ describe("[data] Mutator (Events)", async () => {
async (event) => { async (event) => {
return { return {
...event.params.data, ...event.params.data,
views: event.params.data.views + 1 views: event.params.data.views + 1,
}; };
}, },
"sync" "sync",
); );
const mutator = em.mutator("posts"); const mutator = em.mutator("posts");
@@ -363,7 +363,7 @@ describe("[data] Mutator (Events)", async () => {
expect(result.data).toEqual({ expect(result.data).toEqual({
id: 1, id: 1,
title: "test", title: "test",
views: 3 views: 3,
}); });
}); });
}); });

View File

@@ -7,7 +7,7 @@ import {
LibsqlConnection, LibsqlConnection,
ManyToOneRelation, ManyToOneRelation,
RepositoryEvents, RepositoryEvents,
TextField TextField,
} from "../../../src/data"; } from "../../../src/data";
import { getDummyConnection } from "../helper"; import { getDummyConnection } from "../helper";
@@ -70,13 +70,13 @@ describe("[Repository]", async () => {
const q1 = selectQ(conn).compile(); const q1 = selectQ(conn).compile();
const res = await client.execute({ const res = await client.execute({
sql: q1.sql, sql: q1.sql,
args: q1.parameters as any args: q1.parameters as any,
}); });
const q2 = countQ(conn).compile(); const q2 = countQ(conn).compile();
const count = await client.execute({ const count = await client.execute({
sql: q2.sql, sql: q2.sql,
args: q2.parameters as any args: q2.parameters as any,
}); });
return [res, count]; return [res, count];
} }
@@ -93,7 +93,7 @@ describe("[Repository]", async () => {
const exec = async ( const exec = async (
name: string, name: string,
fn: (em: EntityManager<any>) => Promise<any>, fn: (em: EntityManager<any>) => Promise<any>,
em: EntityManager<any> em: EntityManager<any>,
) => { ) => {
const res = await Perf.execute(() => fn(em), times); const res = await Perf.execute(() => fn(em), times);
await sleep(1000); await sleep(1000);
@@ -102,7 +102,7 @@ describe("[Repository]", async () => {
total: res.total.toFixed(2), total: res.total.toFixed(2),
avg: (res.total / times).toFixed(2), avg: (res.total / times).toFixed(2),
first: res.marks[0].time.toFixed(2), first: res.marks[0].time.toFixed(2),
last: res.marks[res.marks.length - 1].time.toFixed(2) last: res.marks[res.marks.length - 1].time.toFixed(2),
}; };
console.log(info.name, info, res.marks); console.log(info.name, info, res.marks);
return info; return info;
@@ -183,7 +183,7 @@ describe("[data] Repository (Events)", async () => {
const items = new Entity("items", [new TextField("label")]); const items = new Entity("items", [new TextField("label")]);
const categories = new Entity("categories", [new TextField("label")]); const categories = new Entity("categories", [new TextField("label")]);
const em = new EntityManager([items, categories], dummyConnection, [ const em = new EntityManager([items, categories], dummyConnection, [
new ManyToOneRelation(categories, items) new ManyToOneRelation(categories, items),
]); ]);
await em.schema().sync({ force: true }); await em.schema().sync({ force: true });
const events = new Map<string, any>(); const events = new Map<string, any>();

View File

@@ -26,7 +26,7 @@ describe("SchemaManager tests", async () => {
isNullable: true, isNullable: true,
isAutoIncrementing: true, isAutoIncrementing: true,
hasDefaultValue: false, hasDefaultValue: false,
comment: undefined comment: undefined,
}, },
{ {
name: "username", name: "username",
@@ -34,7 +34,7 @@ describe("SchemaManager tests", async () => {
isNullable: true, isNullable: true,
isAutoIncrementing: false, isAutoIncrementing: false,
hasDefaultValue: false, hasDefaultValue: false,
comment: undefined comment: undefined,
}, },
{ {
name: "email", name: "email",
@@ -42,7 +42,7 @@ describe("SchemaManager tests", async () => {
isNullable: true, isNullable: true,
isAutoIncrementing: false, isAutoIncrementing: false,
hasDefaultValue: false, hasDefaultValue: false,
comment: undefined comment: undefined,
}, },
{ {
name: "bio", name: "bio",
@@ -50,8 +50,8 @@ describe("SchemaManager tests", async () => {
isNullable: true, isNullable: true,
isAutoIncrementing: false, isAutoIncrementing: false,
hasDefaultValue: false, hasDefaultValue: false,
comment: undefined comment: undefined,
} },
], ],
indices: [ indices: [
{ {
@@ -61,11 +61,11 @@ describe("SchemaManager tests", async () => {
columns: [ columns: [
{ {
name: "email", name: "email",
order: 0 order: 0,
} },
] ],
} },
] ],
}); });
}); });
@@ -77,10 +77,10 @@ describe("SchemaManager tests", async () => {
new Entity(table, [ new Entity(table, [
new TextField("username"), new TextField("username"),
new TextField("email"), new TextField("email"),
new TextField("bio") new TextField("bio"),
]) ]),
], ],
dummyConnection dummyConnection,
); );
const kysely = em.connection.kysely; const kysely = em.connection.kysely;
@@ -101,8 +101,8 @@ describe("SchemaManager tests", async () => {
name: table, name: table,
isNew: false, isNew: false,
columns: { add: ["bio"], drop: [], change: [] }, columns: { add: ["bio"], drop: [], change: [] },
indices: { add: [], drop: [index] } indices: { add: [], drop: [index] },
} },
]); ]);
// now sync // now sync
@@ -119,7 +119,7 @@ describe("SchemaManager tests", async () => {
const table = "drop_column"; const table = "drop_column";
const em = new EntityManager( const em = new EntityManager(
[new Entity(table, [new TextField("username")])], [new Entity(table, [new TextField("username")])],
dummyConnection dummyConnection,
); );
const kysely = em.connection.kysely; const kysely = em.connection.kysely;
@@ -141,10 +141,10 @@ describe("SchemaManager tests", async () => {
columns: { columns: {
add: [], add: [],
drop: ["email"], drop: ["email"],
change: [] change: [],
}, },
indices: { add: [], drop: [] } indices: { add: [], drop: [] },
} },
]); ]);
// now sync // now sync
@@ -165,15 +165,15 @@ describe("SchemaManager tests", async () => {
new Entity(usersTable, [ new Entity(usersTable, [
new TextField("username"), new TextField("username"),
new TextField("email"), new TextField("email"),
new TextField("bio") new TextField("bio"),
]), ]),
new Entity(postsTable, [ new Entity(postsTable, [
new TextField("title"), new TextField("title"),
new TextField("content"), new TextField("content"),
new TextField("created_at") new TextField("created_at"),
]) ]),
], ],
dummyConnection dummyConnection,
); );
const kysely = em.connection.kysely; const kysely = em.connection.kysely;
@@ -192,7 +192,7 @@ describe("SchemaManager tests", async () => {
name: usersTable, name: usersTable,
isNew: false, isNew: false,
columns: { add: ["bio"], drop: [], change: [] }, columns: { add: ["bio"], drop: [], change: [] },
indices: { add: [], drop: [] } indices: { add: [], drop: [] },
}, },
{ {
name: postsTable, name: postsTable,
@@ -200,10 +200,10 @@ describe("SchemaManager tests", async () => {
columns: { columns: {
add: ["id", "title", "content", "created_at"], add: ["id", "title", "content", "created_at"],
drop: [], drop: [],
change: [] change: [],
}, },
indices: { add: [], drop: [] } indices: { add: [], drop: [] },
} },
]); ]);
// now sync // now sync
@@ -228,8 +228,8 @@ describe("SchemaManager tests", async () => {
name: entity.name, name: entity.name,
isNew: true, isNew: true,
columns: { add: ["id", "email"], drop: [], change: [] }, columns: { add: ["id", "email"], drop: [], change: [] },
indices: { add: [index.name!], drop: [] } indices: { add: [index.name!], drop: [] },
} },
]); ]);
// sync and then check again // sync and then check again
@@ -256,8 +256,8 @@ describe("SchemaManager tests", async () => {
name: entity.name, name: entity.name,
isNew: false, isNew: false,
columns: { add: [], drop: [], change: [] }, columns: { add: [], drop: [], change: [] },
indices: { add: [index.name!], drop: [] } indices: { add: [index.name!], drop: [] },
} },
]); ]);
// sync and then check again // sync and then check again

View File

@@ -1,5 +1,4 @@
import { afterAll, describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { _jsonp } from "../../../src/core/utils";
import { import {
Entity, Entity,
EntityManager, EntityManager,
@@ -7,10 +6,10 @@ import {
ManyToOneRelation, ManyToOneRelation,
PolymorphicRelation, PolymorphicRelation,
TextField, TextField,
WithBuilder WithBuilder,
} from "../../../src/data"; } from "../../../src/data";
import * as proto from "../../../src/data/prototype"; import * as proto from "../../../src/data/prototype";
import { compileQb, prettyPrintQb, schemaToEm } from "../../helper"; import { schemaToEm } from "../../helper";
import { getDummyConnection } from "../helper"; import { getDummyConnection } from "../helper";
const { dummyConnection } = getDummyConnection(); const { dummyConnection } = getDummyConnection();
@@ -21,32 +20,32 @@ describe("[data] WithBuilder", async () => {
{ {
posts: proto.entity("posts", {}), posts: proto.entity("posts", {}),
users: proto.entity("users", {}), users: proto.entity("users", {}),
media: proto.entity("media", {}) media: proto.entity("media", {}),
}, },
({ relation }, { posts, users, media }) => { ({ relation }, { posts, users, media }) => {
relation(posts).manyToOne(users); relation(posts).manyToOne(users);
relation(users).polyToOne(media, { mappedBy: "avatar" }); relation(users).polyToOne(media, { mappedBy: "avatar" });
} },
); );
const em = schemaToEm(schema); const em = schemaToEm(schema);
expect(WithBuilder.validateWiths(em, "posts", undefined)).toBe(0); expect(WithBuilder.validateWiths(em, "posts", undefined as any)).toBe(0);
expect(WithBuilder.validateWiths(em, "posts", {})).toBe(0); expect(WithBuilder.validateWiths(em, "posts", {})).toBe(0);
expect(WithBuilder.validateWiths(em, "posts", { users: {} })).toBe(1); expect(WithBuilder.validateWiths(em, "posts", { users: {} })).toBe(1);
expect( expect(
WithBuilder.validateWiths(em, "posts", { WithBuilder.validateWiths(em, "posts", {
users: { users: {
with: { avatar: {} } with: { avatar: {} },
} },
}) }),
).toBe(2); ).toBe(2);
expect(() => WithBuilder.validateWiths(em, "posts", { author: {} })).toThrow(); expect(() => WithBuilder.validateWiths(em, "posts", { author: {} })).toThrow();
expect(() => expect(() =>
WithBuilder.validateWiths(em, "posts", { WithBuilder.validateWiths(em, "posts", {
users: { users: {
with: { glibberish: {} } with: { glibberish: {} },
} },
}) }),
).toThrow(); ).toThrow();
}); });
@@ -56,8 +55,8 @@ describe("[data] WithBuilder", async () => {
expect(() => expect(() =>
WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, { WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, {
posts: {} posts: {},
}) }),
).toThrow('Relation "users<>posts" not found'); ).toThrow('Relation "users<>posts" not found');
}); });
@@ -68,13 +67,13 @@ describe("[data] WithBuilder", async () => {
const em = new EntityManager([users, posts], dummyConnection, relations); const em = new EntityManager([users, posts], dummyConnection, relations);
const qb = WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, { const qb = WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, {
posts: {} posts: {},
}); });
const res = qb.compile(); const res = qb.compile();
expect(res.sql).toBe( expect(res.sql).toBe(
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'content\', "agg"."content", \'author_id\', "agg"."author_id")), \'[]\') from (select "posts"."id" as "id", "posts"."content" as "content", "posts"."author_id" as "author_id" from "posts" as "posts" where "posts"."author_id" = "users"."id" order by "posts"."id" asc limit ? offset ?) as agg) as "posts" from "users"' 'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'content\', "agg"."content", \'author_id\', "agg"."author_id")), \'[]\') from (select "posts"."id" as "id", "posts"."content" as "content", "posts"."author_id" as "author_id" from "posts" as "posts" where "posts"."author_id" = "users"."id" order by "posts"."id" asc limit ? offset ?) as agg) as "posts" from "users"',
); );
expect(res.parameters).toEqual([10, 0]); expect(res.parameters).toEqual([10, 0]);
@@ -83,14 +82,14 @@ describe("[data] WithBuilder", async () => {
em.connection.kysely.selectFrom("posts"), em.connection.kysely.selectFrom("posts"),
posts, // @todo: try with "users", it gives output! posts, // @todo: try with "users", it gives output!
{ {
author: {} author: {},
} },
); );
const res2 = qb2.compile(); const res2 = qb2.compile();
expect(res2.sql).toBe( expect(res2.sql).toBe(
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "users"."id" as "id", "users"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" order by "users"."id" asc limit ? offset ?) as obj) as "author" from "posts"' 'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "users"."id" as "id", "users"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" order by "users"."id" asc limit ? offset ?) as obj) as "author" from "posts"',
); );
expect(res2.parameters).toEqual([1, 0]); expect(res2.parameters).toEqual([1, 0]);
}); });
@@ -124,7 +123,7 @@ describe("[data] WithBuilder", async () => {
.values([ .values([
{ posts_id: 1, categories_id: 1 }, { posts_id: 1, categories_id: 1 },
{ posts_id: 2, categories_id: 2 }, { posts_id: 2, categories_id: 2 },
{ posts_id: 1, categories_id: 2 } { posts_id: 1, categories_id: 2 },
]) ])
.execute(); .execute();
@@ -138,14 +137,14 @@ describe("[data] WithBuilder", async () => {
title: "fashion post", title: "fashion post",
categories: [ categories: [
{ id: 1, label: "fashion" }, { id: 1, label: "fashion" },
{ id: 2, label: "beauty" } { id: 2, label: "beauty" },
] ],
}, },
{ {
id: 2, id: 2,
title: "beauty post", title: "beauty post",
categories: [{ id: 2, label: "beauty" }] categories: [{ id: 2, label: "beauty" }],
} },
]); ]);
const res2 = await em.repository(categories).findMany({ with: { posts: {} } }); const res2 = await em.repository(categories).findMany({ with: { posts: {} } });
@@ -156,21 +155,21 @@ describe("[data] WithBuilder", async () => {
{ {
id: 1, id: 1,
label: "fashion", label: "fashion",
posts: [{ id: 1, title: "fashion post" }] posts: [{ id: 1, title: "fashion post" }],
}, },
{ {
id: 2, id: 2,
label: "beauty", label: "beauty",
posts: [ posts: [
{ id: 1, title: "fashion post" }, { id: 1, title: "fashion post" },
{ id: 2, title: "beauty post" } { id: 2, title: "beauty post" },
] ],
}, },
{ {
id: 3, id: 3,
label: "tech", label: "tech",
posts: [] posts: [],
} },
]); ]);
}); });
@@ -181,7 +180,7 @@ describe("[data] WithBuilder", async () => {
const entities = [media, categories]; const entities = [media, categories];
const single = new PolymorphicRelation(categories, media, { const single = new PolymorphicRelation(categories, media, {
mappedBy: "single", mappedBy: "single",
targetCardinality: 1 targetCardinality: 1,
}); });
const multiple = new PolymorphicRelation(categories, media, { mappedBy: "multiple" }); const multiple = new PolymorphicRelation(categories, media, { mappedBy: "multiple" });
@@ -191,11 +190,11 @@ describe("[data] WithBuilder", async () => {
em, em,
em.connection.kysely.selectFrom("categories"), em.connection.kysely.selectFrom("categories"),
categories, categories,
{ single: {} } { single: {} },
); );
const res = qb.compile(); const res = qb.compile();
expect(res.sql).toBe( expect(res.sql).toBe(
'select (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as obj) as "single" from "categories"' 'select (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as obj) as "single" from "categories"',
); );
expect(res.parameters).toEqual(["categories.single", 1, 0]); expect(res.parameters).toEqual(["categories.single", 1, 0]);
@@ -203,11 +202,11 @@ describe("[data] WithBuilder", async () => {
em, em,
em.connection.kysely.selectFrom("categories"), em.connection.kysely.selectFrom("categories"),
categories, categories,
{ multiple: {} } { multiple: {} },
); );
const res2 = qb2.compile(); const res2 = qb2.compile();
expect(res2.sql).toBe( expect(res2.sql).toBe(
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'path\', "agg"."path")), \'[]\') from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as agg) as "multiple" from "categories"' 'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'path\', "agg"."path")), \'[]\') from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as agg) as "multiple" from "categories"',
); );
expect(res2.parameters).toEqual(["categories.multiple", 10, 0]); expect(res2.parameters).toEqual(["categories.multiple", 10, 0]);
}); });
@@ -240,16 +239,16 @@ describe("[data] WithBuilder", async () => {
{ {
posts: proto.entity("posts", {}), posts: proto.entity("posts", {}),
users: proto.entity("users", { users: proto.entity("users", {
username: proto.text() username: proto.text(),
}), }),
media: proto.entity("media", { media: proto.entity("media", {
path: proto.text() path: proto.text(),
}) }),
}, },
({ relation }, { posts, users, media }) => { ({ relation }, { posts, users, media }) => {
relation(posts).manyToOne(users); relation(posts).manyToOne(users);
relation(users).polyToOne(media, { mappedBy: "avatar" }); relation(users).polyToOne(media, { mappedBy: "avatar" });
} },
); );
const em = schemaToEm(schema); const em = schemaToEm(schema);
@@ -265,16 +264,16 @@ describe("[data] WithBuilder", async () => {
with: { with: {
avatar: { avatar: {
select: ["id", "path"], select: ["id", "path"],
limit: 2 // ignored limit: 2, // ignored
} },
} },
} },
} },
); );
//prettyPrintQb(qb); //prettyPrintQb(qb);
expect(qb.compile().sql).toBe( expect(qb.compile().sql).toBe(
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username", \'avatar\', "obj"."avatar") from (select "users"."id" as "id", "users"."username" as "username", (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "users"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as obj) as "avatar" from "users" as "users" where "users"."id" = "posts"."users_id" order by "users"."username" asc limit ? offset ?) as obj) as "users" from "posts"' 'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username", \'avatar\', "obj"."avatar") from (select "users"."id" as "id", "users"."username" as "username", (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "users"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as obj) as "avatar" from "users" as "users" where "users"."id" = "posts"."users_id" order by "users"."username" asc limit ? offset ?) as obj) as "users" from "posts"',
); );
expect(qb.compile().parameters).toEqual(["users.avatar", 1, 0, 1, 0]); expect(qb.compile().parameters).toEqual(["users.avatar", 1, 0, 1, 0]);
}); });
@@ -285,17 +284,17 @@ describe("[data] WithBuilder", async () => {
posts: proto.entity("posts", {}), posts: proto.entity("posts", {}),
comments: proto.entity("comments", {}), comments: proto.entity("comments", {}),
users: proto.entity("users", { users: proto.entity("users", {
username: proto.text() username: proto.text(),
}), }),
media: proto.entity("media", { media: proto.entity("media", {
path: proto.text() path: proto.text(),
}) }),
}, },
({ relation }, { posts, comments, users, media }) => { ({ relation }, { posts, comments, users, media }) => {
relation(posts).manyToOne(users).polyToOne(media, { mappedBy: "images" }); relation(posts).manyToOne(users).polyToOne(media, { mappedBy: "images" });
relation(users).polyToOne(media, { mappedBy: "avatar" }); relation(users).polyToOne(media, { mappedBy: "avatar" });
relation(comments).manyToOne(posts).manyToOne(users); relation(comments).manyToOne(posts).manyToOne(users);
} },
); );
const em = schemaToEm(schema); const em = schemaToEm(schema);
@@ -308,15 +307,15 @@ describe("[data] WithBuilder", async () => {
limit: 12, limit: 12,
with: { with: {
users: { users: {
select: ["username"] select: ["username"],
} },
} },
} },
} },
); );
expect(qb.compile().sql).toBe( expect(qb.compile().sql).toBe(
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'posts_id\', "agg"."posts_id", \'users_id\', "agg"."users_id", \'users\', "agg"."users")), \'[]\') from (select "comments"."id" as "id", "comments"."posts_id" as "posts_id", "comments"."users_id" as "users_id", (select json_object(\'username\', "obj"."username") from (select "users"."username" as "username" from "users" as "users" where "users"."id" = "comments"."users_id" order by "users"."id" asc limit ? offset ?) as obj) as "users" from "comments" as "comments" where "comments"."posts_id" = "posts"."id" order by "comments"."id" asc limit ? offset ?) as agg) as "comments" from "posts"' 'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'posts_id\', "agg"."posts_id", \'users_id\', "agg"."users_id", \'users\', "agg"."users")), \'[]\') from (select "comments"."id" as "id", "comments"."posts_id" as "posts_id", "comments"."users_id" as "users_id", (select json_object(\'username\', "obj"."username") from (select "users"."username" as "username" from "users" as "users" where "users"."id" = "comments"."users_id" order by "users"."id" asc limit ? offset ?) as obj) as "users" from "comments" as "comments" where "comments"."posts_id" = "posts"."id" order by "comments"."id" asc limit ? offset ?) as agg) as "comments" from "posts"',
); );
expect(qb.compile().parameters).toEqual([1, 0, 12, 0]); expect(qb.compile().parameters).toEqual([1, 0, 12, 0]);
}); });
@@ -325,23 +324,23 @@ describe("[data] WithBuilder", async () => {
const schema = proto.em( const schema = proto.em(
{ {
posts: proto.entity("posts", { posts: proto.entity("posts", {
title: proto.text() title: proto.text(),
}), }),
comments: proto.entity("comments", { comments: proto.entity("comments", {
content: proto.text() content: proto.text(),
}), }),
users: proto.entity("users", { users: proto.entity("users", {
username: proto.text() username: proto.text(),
}), }),
media: proto.entity("media", { media: proto.entity("media", {
path: proto.text() path: proto.text(),
}) }),
}, },
({ relation }, { posts, comments, users, media }) => { ({ relation }, { posts, comments, users, media }) => {
relation(posts).manyToOne(users).polyToOne(media, { mappedBy: "images" }); relation(posts).manyToOne(users).polyToOne(media, { mappedBy: "images" });
relation(users).polyToOne(media, { mappedBy: "avatar" }); relation(users).polyToOne(media, { mappedBy: "avatar" });
relation(comments).manyToOne(posts).manyToOne(users); relation(comments).manyToOne(posts).manyToOne(users);
} },
); );
const em = schemaToEm(schema); const em = schemaToEm(schema);
await em.schema().sync({ force: true }); await em.schema().sync({ force: true });
@@ -351,7 +350,7 @@ describe("[data] WithBuilder", async () => {
await em.mutator("posts").insertMany([ await em.mutator("posts").insertMany([
{ title: "post1", users_id: 1 }, { title: "post1", users_id: 1 },
{ title: "post2", users_id: 1 }, { title: "post2", users_id: 1 },
{ title: "post3", users_id: 2 } { title: "post3", users_id: 2 },
]); ]);
await em.mutator("comments").insertMany([ await em.mutator("comments").insertMany([
{ content: "comment1", posts_id: 1, users_id: 1 }, { content: "comment1", posts_id: 1, users_id: 1 },
@@ -360,7 +359,7 @@ describe("[data] WithBuilder", async () => {
{ content: "comment3", posts_id: 2, users_id: 1 }, { content: "comment3", posts_id: 2, users_id: 1 },
{ content: "comment4", posts_id: 2, users_id: 2 }, { content: "comment4", posts_id: 2, users_id: 2 },
{ content: "comment5", posts_id: 3, users_id: 1 }, { content: "comment5", posts_id: 3, users_id: 1 },
{ content: "comment6", posts_id: 3, users_id: 2 } { content: "comment6", posts_id: 3, users_id: 2 },
]); ]);
const result = await em.repo("posts").findMany({ const result = await em.repo("posts").findMany({
@@ -371,11 +370,11 @@ describe("[data] WithBuilder", async () => {
select: ["content"], select: ["content"],
with: { with: {
users: { users: {
select: ["username"] select: ["username"],
} },
} },
} },
} },
}); });
expect(result.data).toEqual([ expect(result.data).toEqual([
@@ -385,16 +384,16 @@ describe("[data] WithBuilder", async () => {
{ {
content: "comment1", content: "comment1",
users: { users: {
username: "user1" username: "user1",
} },
}, },
{ {
content: "comment1-1", content: "comment1-1",
users: { users: {
username: "user1" username: "user1",
} },
} },
] ],
}, },
{ {
title: "post2", title: "post2",
@@ -402,16 +401,16 @@ describe("[data] WithBuilder", async () => {
{ {
content: "comment3", content: "comment3",
users: { users: {
username: "user1" username: "user1",
} },
}, },
{ {
content: "comment4", content: "comment4",
users: { users: {
username: "user2" username: "user2",
} },
} },
] ],
}, },
{ {
title: "post3", title: "post3",
@@ -419,17 +418,17 @@ describe("[data] WithBuilder", async () => {
{ {
content: "comment5", content: "comment5",
users: { users: {
username: "user1" username: "user1",
} },
}, },
{ {
content: "comment6", content: "comment6",
users: { users: {
username: "user2" username: "user2",
} },
} },
] ],
} },
]); ]);
//console.log(_jsonp(result.data)); //console.log(_jsonp(result.data));
}); });

View File

@@ -22,10 +22,10 @@ describe("Connection", async () => {
columns: [ columns: [
{ {
name: "name", name: "name",
order: 0 order: 0,
} },
] ],
} },
]); ]);
}); });
@@ -54,14 +54,14 @@ describe("Connection", async () => {
columns: [ columns: [
{ {
name: "name", name: "name",
order: 0 order: 0,
}, },
{ {
name: "desc", name: "desc",
order: 1 order: 1,
} },
] ],
} },
]); ]);
}); });
@@ -83,10 +83,10 @@ describe("Connection", async () => {
columns: [ columns: [
{ {
name: "name", name: "name",
order: 0 order: 0,
} },
] ],
} },
]); ]);
}); });
}); });

View File

@@ -10,12 +10,12 @@ describe("[data] EnumField", async () => {
runBaseFieldTests( runBaseFieldTests(
EnumField, EnumField,
{ defaultValue: "a", schemaType: "text" }, { defaultValue: "a", schemaType: "text" },
{ options: options(["a", "b", "c"]) } { options: options(["a", "b", "c"]) },
); );
test("yields if default value is not a valid option", async () => { test("yields if default value is not a valid option", async () => {
expect( expect(
() => new EnumField("test", { options: options(["a", "b"]), default_value: "c" }) () => new EnumField("test", { options: options(["a", "b"]), default_value: "c" }),
).toThrow(); ).toThrow();
}); });
@@ -31,7 +31,7 @@ describe("[data] EnumField", async () => {
const field = new EnumField("test", { const field = new EnumField("test", {
options: options(["a", "b", "c"]), options: options(["a", "b", "c"]),
default_value: "a", default_value: "a",
required: true required: true,
}); });
expect(field.transformRetrieve(null)).toBe("a"); expect(field.transformRetrieve(null)).toBe("a");

View File

@@ -24,20 +24,20 @@ describe("[data] Field", async () => {
const required = new FieldSpec("test", { required: true }); const required = new FieldSpec("test", { required: true });
const requiredDefault = new FieldSpec("test", { const requiredDefault = new FieldSpec("test", {
required: true, required: true,
default_value: "test" default_value: "test",
}); });
expect(required.transformPersist(null, undefined as any, undefined as any)).rejects.toThrow(); expect(required.transformPersist(null, undefined as any, undefined as any)).rejects.toThrow();
expect( expect(
required.transformPersist(undefined, undefined as any, undefined as any) required.transformPersist(undefined, undefined as any, undefined as any),
).rejects.toThrow(); ).rejects.toThrow();
// works because it has a default value // works because it has a default value
expect( expect(
requiredDefault.transformPersist(null, undefined as any, undefined as any) requiredDefault.transformPersist(null, undefined as any, undefined as any),
).resolves.toBeDefined(); ).resolves.toBeDefined();
expect( expect(
requiredDefault.transformPersist(undefined, undefined as any, undefined as any) requiredDefault.transformPersist(undefined, undefined as any, undefined as any),
).resolves.toBeDefined(); ).resolves.toBeDefined();
}); });
}); });

View File

@@ -5,7 +5,7 @@ import {
EntityIndex, EntityIndex,
type EntityManager, type EntityManager,
Field, Field,
type SchemaResponse type SchemaResponse,
} from "../../../../src/data"; } from "../../../../src/data";
class TestField extends Field { class TestField extends Field {

View File

@@ -7,7 +7,7 @@ describe("[data] JsonField", async () => {
runBaseFieldTests(JsonField, { runBaseFieldTests(JsonField, {
defaultValue: { a: 1 }, defaultValue: { a: 1 },
sampleValues: ["string", { test: 1 }, 1], sampleValues: ["string", { test: 1 }, 1],
schemaType: "text" schemaType: "text",
}); });
test("transformPersist (no config)", async () => { test("transformPersist (no config)", async () => {

View File

@@ -18,7 +18,7 @@ export function transformPersist(field: Field, value: any, context?: TActionCont
export function runBaseFieldTests( export function runBaseFieldTests(
fieldClass: ConstructableField, fieldClass: ConstructableField,
config: FieldTestConfig, config: FieldTestConfig,
_requiredConfig: any = {} _requiredConfig: any = {},
) { ) {
const noConfigField = new fieldClass("no_config", _requiredConfig); const noConfigField = new fieldClass("no_config", _requiredConfig);
const fillable = new fieldClass("fillable", { ..._requiredConfig, fillable: true }); const fillable = new fieldClass("fillable", { ..._requiredConfig, fillable: true });
@@ -29,7 +29,7 @@ export function runBaseFieldTests(
..._requiredConfig, ..._requiredConfig,
fillable: true, fillable: true,
required: true, required: true,
default_value: config.defaultValue default_value: config.defaultValue,
}); });
test("schema", () => { test("schema", () => {
@@ -37,7 +37,7 @@ export function runBaseFieldTests(
expect(noConfigField.schema(null as any)).toEqual([ expect(noConfigField.schema(null as any)).toEqual([
"no_config", "no_config",
config.schemaType, config.schemaType,
expect.any(Function) expect.any(Function),
]); ]);
}); });
@@ -96,7 +96,7 @@ export function runBaseFieldTests(
//order: 1, //order: 1,
fillable: true, fillable: true,
required: false, required: false,
hidden: false hidden: false,
//virtual: false, //virtual: false,
//default_value: undefined //default_value: undefined
}; };
@@ -105,20 +105,20 @@ export function runBaseFieldTests(
const json = field.toJSON(); const json = field.toJSON();
return { return {
...json, ...json,
config: omit(json.config, ["html"]) config: omit(json.config, ["html"]),
}; };
} }
expect(fieldJson(noConfigField)).toEqual({ expect(fieldJson(noConfigField)).toEqual({
//name: "no_config", //name: "no_config",
type: noConfigField.type, type: noConfigField.type,
config: _config config: _config,
}); });
expect(fieldJson(fillable)).toEqual({ expect(fieldJson(fillable)).toEqual({
//name: "fillable", //name: "fillable",
type: noConfigField.type, type: noConfigField.type,
config: _config config: _config,
}); });
expect(fieldJson(required)).toEqual({ expect(fieldJson(required)).toEqual({
@@ -126,8 +126,8 @@ export function runBaseFieldTests(
type: required.type, type: required.type,
config: { config: {
..._config, ..._config,
required: true required: true,
} },
}); });
expect(fieldJson(hidden)).toEqual({ expect(fieldJson(hidden)).toEqual({
@@ -135,8 +135,8 @@ export function runBaseFieldTests(
type: required.type, type: required.type,
config: { config: {
..._config, ..._config,
hidden: true hidden: true,
} },
}); });
expect(fieldJson(dflt)).toEqual({ expect(fieldJson(dflt)).toEqual({
@@ -144,8 +144,8 @@ export function runBaseFieldTests(
type: dflt.type, type: dflt.type,
config: { config: {
..._config, ..._config,
default_value: config.defaultValue default_value: config.defaultValue,
} },
}); });
expect(fieldJson(requiredAndDefault)).toEqual({ expect(fieldJson(requiredAndDefault)).toEqual({
@@ -155,8 +155,8 @@ export function runBaseFieldTests(
..._config, ..._config,
fillable: true, fillable: true,
required: true, required: true,
default_value: config.defaultValue default_value: config.defaultValue,
} },
}); });
}); });
} }

View File

@@ -4,7 +4,7 @@ import {
type BaseRelationConfig, type BaseRelationConfig,
EntityRelation, EntityRelation,
EntityRelationAnchor, EntityRelationAnchor,
RelationTypes RelationTypes,
} from "../../../../src/data/relations"; } from "../../../../src/data/relations";
class TestEntityRelation extends EntityRelation { class TestEntityRelation extends EntityRelation {
@@ -12,7 +12,7 @@ class TestEntityRelation extends EntityRelation {
super( super(
new EntityRelationAnchor(new Entity("source"), "source"), new EntityRelationAnchor(new Entity("source"), "source"),
new EntityRelationAnchor(new Entity("target"), "target"), new EntityRelationAnchor(new Entity("target"), "target"),
config config,
); );
} }
initialize(em: EntityManager<any>) {} initialize(em: EntityManager<any>) {}

View File

@@ -30,14 +30,14 @@ beforeAll(() =>
method: init?.method ?? "GET", method: init?.method ?? "GET",
// @ts-ignore // @ts-ignore
headers: Object.fromEntries(init?.headers?.entries() ?? []), headers: Object.fromEntries(init?.headers?.entries() ?? []),
body: init?.body body: init?.body,
}; };
return new Response(JSON.stringify({ todos: [1, 2], request }), { return new Response(JSON.stringify({ todos: [1, 2], request }), {
status: 200, status: 200,
headers: { "Content-Type": "application/json" } headers: { "Content-Type": "application/json" },
}); });
}) }),
); );
afterAll(unmockFetch); afterAll(unmockFetch);
@@ -46,7 +46,7 @@ describe("FetchTask", async () => {
const task = new FetchTask("Fetch Something", { const task = new FetchTask("Fetch Something", {
url: "https://jsonplaceholder.typicode.com/todos/1", url: "https://jsonplaceholder.typicode.com/todos/1",
method: "GET", method: "GET",
headers: [{ key: "Content-Type", value: "application/json" }] headers: [{ key: "Content-Type", value: "application/json" }],
}); });
const result = await task.run(); const result = await task.run();
@@ -62,18 +62,18 @@ describe("FetchTask", async () => {
expect( expect(
// // @ts-expect-error // // @ts-expect-error
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: 1 }) () => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: 1 }),
).toThrow(); ).toThrow();
expect( expect(
new FetchTask("", { new FetchTask("", {
url: "https://jsonplaceholder.typicode.com", url: "https://jsonplaceholder.typicode.com",
method: "invalid" method: "invalid",
}).execute() }).execute(),
).rejects.toThrow(/^Invalid method/); ).rejects.toThrow(/^Invalid method/);
expect( expect(
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: "GET" }) () => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: "GET" }),
).toBeDefined(); ).toBeDefined();
expect(() => new FetchTask("", { url: "", method: "Invalid" })).toThrow(); expect(() => new FetchTask("", { url: "", method: "Invalid" })).toThrow();
@@ -85,17 +85,17 @@ describe("FetchTask", async () => {
method: "{{ flow.output.method }}", method: "{{ flow.output.method }}",
headers: [ headers: [
{ key: "Content-{{ flow.output.headerKey }}", value: "application/json" }, { key: "Content-{{ flow.output.headerKey }}", value: "application/json" },
{ key: "Authorization", value: "Bearer {{ flow.output.apiKey }}" } { key: "Authorization", value: "Bearer {{ flow.output.apiKey }}" },
], ],
body: JSON.stringify({ body: JSON.stringify({
email: "{{ flow.output.email }}" email: "{{ flow.output.email }}",
}) }),
}); });
const inputs = { const inputs = {
headerKey: "Type", headerKey: "Type",
apiKey: 123, apiKey: 123,
email: "what@else.com", email: "what@else.com",
method: "PATCH" method: "PATCH",
}; };
const flow = new Flow("", [task]); const flow = new Flow("", [task]);

View File

@@ -4,16 +4,16 @@ import { Flow, LogTask, RenderTask, SubFlowTask } from "../../src/flows";
describe("SubFlowTask", async () => { describe("SubFlowTask", async () => {
test("Simple Subflow", async () => { test("Simple Subflow", async () => {
const subTask = new RenderTask("render", { const subTask = new RenderTask("render", {
render: "subflow" render: "subflow",
}); });
const subflow = new Flow("subflow", [subTask]); const subflow = new Flow("subflow", [subTask]);
const task = new LogTask("log"); const task = new LogTask("log");
const task2 = new SubFlowTask("sub", { const task2 = new SubFlowTask("sub", {
flow: subflow flow: subflow,
}); });
const task3 = new RenderTask("render2", { const task3 = new RenderTask("render2", {
render: "Subflow output: {{ sub.output }}" render: "Subflow output: {{ sub.output }}",
}); });
const flow = new Flow("test", [task, task2, task3], []); const flow = new Flow("test", [task, task2, task3], []);
@@ -30,7 +30,7 @@ describe("SubFlowTask", async () => {
test("Simple loop", async () => { test("Simple loop", async () => {
const subTask = new RenderTask("render", { const subTask = new RenderTask("render", {
render: "run {{ flow.output }}" render: "run {{ flow.output }}",
}); });
const subflow = new Flow("subflow", [subTask]); const subflow = new Flow("subflow", [subTask]);
@@ -38,10 +38,10 @@ describe("SubFlowTask", async () => {
const task2 = new SubFlowTask("sub", { const task2 = new SubFlowTask("sub", {
flow: subflow, flow: subflow,
loop: true, loop: true,
input: [1, 2, 3] input: [1, 2, 3],
}); });
const task3 = new RenderTask("render2", { const task3 = new RenderTask("render2", {
render: `Subflow output: {{ sub.output | join: ", " }}` render: `Subflow output: {{ sub.output | join: ", " }}`,
}); });
const flow = new Flow("test", [task, task2, task3], []); const flow = new Flow("test", [task, task2, task3], []);
@@ -61,7 +61,7 @@ describe("SubFlowTask", async () => {
test("Simple loop from flow input", async () => { test("Simple loop from flow input", async () => {
const subTask = new RenderTask("render", { const subTask = new RenderTask("render", {
render: "run {{ flow.output }}" render: "run {{ flow.output }}",
}); });
const subflow = new Flow("subflow", [subTask]); const subflow = new Flow("subflow", [subTask]);
@@ -70,10 +70,10 @@ describe("SubFlowTask", async () => {
const task2 = new SubFlowTask("sub", { const task2 = new SubFlowTask("sub", {
flow: subflow, flow: subflow,
loop: true, loop: true,
input: "{{ flow.output | json }}" input: "{{ flow.output | json }}",
}); });
const task3 = new RenderTask("render2", { const task3 = new RenderTask("render2", {
render: `Subflow output: {{ sub.output | join: ", " }}` render: `Subflow output: {{ sub.output | join: ", " }}`,
}); });
const flow = new Flow("test", [task, task2, task3], []); const flow = new Flow("test", [task, task2, task3], []);

View File

@@ -8,13 +8,13 @@ describe("Task", async () => {
const result = await Task.resolveParams( const result = await Task.resolveParams(
Type.Object({ test: dynamic(Type.Number()) }), Type.Object({ test: dynamic(Type.Number()) }),
{ {
test: "{{ some.path }}" test: "{{ some.path }}",
}, },
{ {
some: { some: {
path: 1 path: 1,
} },
} },
); );
expect(result.test).toBe(1); expect(result.test).toBe(1);
@@ -24,13 +24,13 @@ describe("Task", async () => {
const result = await Task.resolveParams( const result = await Task.resolveParams(
Type.Object({ test: Type.String() }), Type.Object({ test: Type.String() }),
{ {
test: "{{ some.path }}" test: "{{ some.path }}",
}, },
{ {
some: { some: {
path: "1/1" path: "1/1",
} },
} },
); );
expect(result.test).toBe("1/1"); expect(result.test).toBe("1/1");
@@ -40,13 +40,13 @@ describe("Task", async () => {
const result = await Task.resolveParams( const result = await Task.resolveParams(
Type.Object({ test: dynamic(Type.Object({ key: Type.String(), value: Type.String() })) }), Type.Object({ test: dynamic(Type.Object({ key: Type.String(), value: Type.String() })) }),
{ {
test: { key: "path", value: "{{ some.path }}" } test: { key: "path", value: "{{ some.path }}" },
}, },
{ {
some: { some: {
path: "1/1" path: "1/1",
} },
} },
); );
expect(result.test).toEqual({ key: "path", value: "1/1" }); expect(result.test).toEqual({ key: "path", value: "1/1" });
@@ -55,17 +55,17 @@ describe("Task", async () => {
test("resolveParams: with json", async () => { test("resolveParams: with json", async () => {
const result = await Task.resolveParams( const result = await Task.resolveParams(
Type.Object({ Type.Object({
test: dynamic(Type.Object({ key: Type.String(), value: Type.String() })) test: dynamic(Type.Object({ key: Type.String(), value: Type.String() })),
}), }),
{ {
test: "{{ some | json }}" test: "{{ some | json }}",
}, },
{ {
some: { some: {
key: "path", key: "path",
value: "1/1" value: "1/1",
} },
} },
); );
expect(result.test).toEqual({ key: "path", value: "1/1" }); expect(result.test).toEqual({ key: "path", value: "1/1" });
@@ -74,11 +74,11 @@ describe("Task", async () => {
test("resolveParams: with array", async () => { test("resolveParams: with array", async () => {
const result = await Task.resolveParams( const result = await Task.resolveParams(
Type.Object({ Type.Object({
test: dynamic(Type.Array(Type.String())) test: dynamic(Type.Array(Type.String())),
}), }),
{ {
test: '{{ "1,2,3" | split: "," | json }}' test: '{{ "1,2,3" | split: "," | json }}',
} },
); );
expect(result.test).toEqual(["1", "2", "3"]); expect(result.test).toEqual(["1", "2", "3"]);
@@ -87,11 +87,11 @@ describe("Task", async () => {
test("resolveParams: boolean", async () => { test("resolveParams: boolean", async () => {
const result = await Task.resolveParams( const result = await Task.resolveParams(
Type.Object({ Type.Object({
test: dynamic(Type.Boolean()) test: dynamic(Type.Boolean()),
}), }),
{ {
test: "{{ true }}" test: "{{ true }}",
} },
); );
expect(result.test).toEqual(true); expect(result.test).toEqual(true);
@@ -100,11 +100,11 @@ describe("Task", async () => {
test("resolveParams: float", async () => { test("resolveParams: float", async () => {
const result = await Task.resolveParams( const result = await Task.resolveParams(
Type.Object({ Type.Object({
test: dynamic(Type.Number(), Number.parseFloat) test: dynamic(Type.Number(), Number.parseFloat),
}), }),
{ {
test: "{{ 3.14 }}" test: "{{ 3.14 }}",
} },
); );
expect(result.test).toEqual(3.14); expect(result.test).toEqual(3.14);

View File

@@ -7,11 +7,11 @@ const first = getNamedTask(
//throw new Error("Error"); //throw new Error("Error");
return { return {
inner: { inner: {
result: 2 result: 2,
} },
}; };
}, },
1000 1000,
); );
const second = getNamedTask("second (if match)"); const second = getNamedTask("second (if match)");
const third = getNamedTask("third (if error)"); const third = getNamedTask("third (if error)");

View File

@@ -11,7 +11,7 @@ class ExecTask extends Task {
constructor( constructor(
name: string, name: string,
params: any, params: any,
private fn: () => any private fn: () => any,
) { ) {
super(name, params); super(name, params);
} }
@@ -54,8 +54,8 @@ export function getNamedTask(name: string, _func?: () => Promise<any>, delay?: n
return new ExecTask( return new ExecTask(
name, name,
{ {
delay delay,
}, },
func func,
); );
} }

View File

@@ -4,7 +4,7 @@ const first = new LogTask("First", { delay: 1000 });
const second = new LogTask("Second", { delay: 1000 }); const second = new LogTask("Second", { delay: 1000 });
const third = new LogTask("Long Third", { delay: 2500 }); const third = new LogTask("Long Third", { delay: 2500 });
const fourth = new FetchTask("Fetch Something", { const fourth = new FetchTask("Fetch Something", {
url: "https://jsonplaceholder.typicode.com/todos/1" url: "https://jsonplaceholder.typicode.com/todos/1",
}); });
const fifth = new LogTask("Task 4", { delay: 500 }); // without connection const fifth = new LogTask("Task 4", { delay: 500 }); // without connection

View File

@@ -23,10 +23,10 @@ class OutputParamTask extends Task<typeof OutputParamTask.schema> {
static override schema = Type.Object({ static override schema = Type.Object({
number: dynamic( number: dynamic(
Type.Number({ Type.Number({
title: "Output number" title: "Output number",
}), }),
Number.parseInt Number.parseInt,
) ),
}); });
async execute(inputs: InputsMap) { async execute(inputs: InputsMap) {
@@ -75,7 +75,7 @@ describe("Flow task inputs", async () => {
test("output/input", async () => { test("output/input", async () => {
const task = new OutputParamTask("task1", { number: 111 }); const task = new OutputParamTask("task1", { number: 111 });
const task2 = new OutputParamTask("task2", { const task2 = new OutputParamTask("task2", {
number: "{{ task1.output }}" number: "{{ task1.output }}",
}); });
const flow = new Flow("test", [task, task2]); const flow = new Flow("test", [task, task2]);
@@ -94,10 +94,10 @@ describe("Flow task inputs", async () => {
test("input from flow", async () => { test("input from flow", async () => {
const task = new OutputParamTask("task1", { const task = new OutputParamTask("task1", {
number: "{{flow.output.someFancyParam}}" number: "{{flow.output.someFancyParam}}",
}); });
const task2 = new OutputParamTask("task2", { const task2 = new OutputParamTask("task2", {
number: "{{task1.output}}" number: "{{task1.output}}",
}); });
const flow = new Flow("test", [task, task2]); const flow = new Flow("test", [task, task2]);
@@ -126,7 +126,7 @@ describe("Flow task inputs", async () => {
const emgr = new EventManager({ EventTriggerClass }); const emgr = new EventManager({ EventTriggerClass });
const task = new OutputParamTask("event", { const task = new OutputParamTask("event", {
number: "{{flow.output.number}}" number: "{{flow.output.number}}",
}); });
const flow = new Flow( const flow = new Flow(
"test", "test",
@@ -134,8 +134,8 @@ describe("Flow task inputs", async () => {
[], [],
new EventTrigger({ new EventTrigger({
event: "test-event", event: "test-event",
mode: "sync" mode: "sync",
}) }),
); );
flow.setRespondingTask(task); flow.setRespondingTask(task);
flow.trigger.register(flow, emgr); flow.trigger.register(flow, emgr);
@@ -155,8 +155,8 @@ describe("Flow task inputs", async () => {
new HttpTrigger({ new HttpTrigger({
path: "/test", path: "/test",
method: "GET", method: "GET",
mode: "sync" mode: "sync",
}) }),
); );
flow.setRespondingTask(task); flow.setRespondingTask(task);

View File

@@ -10,7 +10,7 @@ const flows = {
back, back,
fanout, fanout,
parallel, parallel,
simpleFetch simpleFetch,
}; };
const arg = process.argv[2]; const arg = process.argv[2];
@@ -32,7 +32,7 @@ const colors = [
"#F78F1E", // Saffron "#F78F1E", // Saffron
"#BD10E0", // Vivid Purple "#BD10E0", // Vivid Purple
"#50E3C2", // Turquoise "#50E3C2", // Turquoise
"#9013FE" // Grape "#9013FE", // Grape
]; ];
const colorsCache: Record<string, string> = {}; const colorsCache: Record<string, string> = {};
@@ -82,7 +82,7 @@ function TerminalFlow({ flow }: { flow: Flow }) {
} }
return t; return t;
}) }),
); );
} }
}); });
@@ -92,7 +92,7 @@ function TerminalFlow({ flow }: { flow: Flow }) {
console.log("done", response ? response : "(no response)"); console.log("done", response ? response : "(no response)");
console.log( console.log(
"Executed tasks:", "Executed tasks:",
execution.logs.map((l) => l.task.name) execution.logs.map((l) => l.task.name),
); );
console.log("Executed count:", execution.logs.length); console.log("Executed count:", execution.logs.length);
}); });

View File

@@ -11,7 +11,7 @@ class ExecTask extends Task {
constructor( constructor(
name: string, name: string,
params: any, params: any,
private fn: () => any private fn: () => any,
) { ) {
super(name, params); super(name, params);
} }
@@ -60,7 +60,7 @@ describe("Flow trigger", async () => {
"test", "test",
[task], [task],
[], [],
new EventTrigger({ event: "test-event", mode: "sync" }) new EventTrigger({ event: "test-event", mode: "sync" }),
); );
flow.trigger.register(flow, emgr); flow.trigger.register(flow, emgr);
@@ -107,8 +107,8 @@ describe("Flow trigger", async () => {
new HttpTrigger({ new HttpTrigger({
path: "/test", path: "/test",
method: "GET", method: "GET",
mode: "sync" mode: "sync",
}) }),
); );
const hono = new Hono(); const hono = new Hono();
@@ -123,7 +123,7 @@ describe("Flow trigger", async () => {
test("http trigger with response", async () => { test("http trigger with response", async () => {
const task = ExecTask.create("http", () => ({ const task = ExecTask.create("http", () => ({
called: true called: true,
})); }));
const flow = new Flow( const flow = new Flow(
"test", "test",
@@ -132,8 +132,8 @@ describe("Flow trigger", async () => {
new HttpTrigger({ new HttpTrigger({
path: "/test", path: "/test",
method: "GET", method: "GET",
mode: "sync" mode: "sync",
}) }),
); );
flow.setRespondingTask(task); flow.setRespondingTask(task);

View File

@@ -11,13 +11,13 @@ class ExecTask extends Task<typeof ExecTask.schema> {
type = "exec"; type = "exec";
static override schema = Type.Object({ static override schema = Type.Object({
delay: Type.Number({ default: 10 }) delay: Type.Number({ default: 10 }),
}); });
constructor( constructor(
name: string, name: string,
params: Static<typeof ExecTask.schema>, params: Static<typeof ExecTask.schema>,
private func: () => Promise<any> private func: () => Promise<any>,
) { ) {
super(name, params); super(name, params);
} }
@@ -36,12 +36,12 @@ function getTask(num: number = 0, delay: number = 5) {
return new ExecTask( return new ExecTask(
`Task ${num}`, `Task ${num}`,
{ {
delay delay,
}, },
async () => { async () => {
//console.log(`[DONE] Task: ${num}`); //console.log(`[DONE] Task: ${num}`);
return true; return true;
} },
); );
//return new LogTask(`Log ${num}`, { delay }); //return new LogTask(`Log ${num}`, { delay });
} }
@@ -56,9 +56,9 @@ function getNamedTask(name: string, _func?: () => Promise<any>, delay?: number)
return new ExecTask( return new ExecTask(
name, name,
{ {
delay: delay ?? 0 delay: delay ?? 0,
}, },
func func,
); );
} }
@@ -228,7 +228,7 @@ describe("Flow tests", async () => {
back back
.task(third) .task(third)
.getOutTasks() .getOutTasks()
.map((t) => t.name) .map((t) => t.name),
).toEqual(["second", "fourth"]); ).toEqual(["second", "fourth"]);
const execution = back.createExecution(); const execution = back.createExecution();
@@ -263,7 +263,7 @@ describe("Flow tests", async () => {
back back
.task(third) .task(third)
.getOutTasks() .getOutTasks()
.map((t) => t.name) .map((t) => t.name),
).toEqual(["second", "fourth"]); ).toEqual(["second", "fourth"]);
const execution = back.createExecution(); const execution = back.createExecution();
@@ -324,8 +324,8 @@ describe("Flow tests", async () => {
const first = getNamedTask("first", async () => { const first = getNamedTask("first", async () => {
return { return {
inner: { inner: {
result: 2 result: 2,
} },
}; };
}); });
const second = getNamedTask("second"); const second = getNamedTask("second");
@@ -361,7 +361,7 @@ describe("Flow tests", async () => {
"[event]", "[event]",
event.isStart() ? "start" : "end", event.isStart() ? "start" : "end",
event.task().name, event.task().name,
event.isStart() ? undefined : event.succeeded() event.isStart() ? undefined : event.succeeded(),
); );
} }
}); });
@@ -389,7 +389,7 @@ describe("Flow tests", async () => {
const second = new LogTask("Task 1"); const second = new LogTask("Task 1");
const third = new LogTask("Task 2", { delay: 50 }); const third = new LogTask("Task 2", { delay: 50 });
const fourth = new FetchTask("Fetch Something", { const fourth = new FetchTask("Fetch Something", {
url: "https://jsonplaceholder.typicode.com/todos/1" url: "https://jsonplaceholder.typicode.com/todos/1",
}); });
const fifth = new LogTask("Task 4"); // without connection const fifth = new LogTask("Task 4"); // without connection
@@ -405,7 +405,7 @@ describe("Flow tests", async () => {
// @todo: fix // @todo: fix
const deserialized = Flow.fromObject("", original, { const deserialized = Flow.fromObject("", original, {
fetch: { cls: FetchTask }, fetch: { cls: FetchTask },
log: { cls: LogTask } log: { cls: LogTask },
} as any); } as any);
const diffdeep = getObjectDiff(original, deserialized.toJSON()); const diffdeep = getObjectDiff(original, deserialized.toJSON());
@@ -414,7 +414,7 @@ describe("Flow tests", async () => {
expect(flow.startTask.name).toEqual(deserialized.startTask.name); expect(flow.startTask.name).toEqual(deserialized.startTask.name);
expect(flow.respondingTask?.name).toEqual( expect(flow.respondingTask?.name).toEqual(
// @ts-ignore // @ts-ignore
deserialized.respondingTask?.name deserialized.respondingTask?.name,
); );
//console.log("--- creating original sequence"); //console.log("--- creating original sequence");

View File

@@ -4,6 +4,9 @@ import Database from "libsql";
import { format as sqlFormat } from "sql-formatter"; import { format as sqlFormat } from "sql-formatter";
import { type Connection, EntityManager, SqliteLocalConnection } from "../src/data"; import { type Connection, EntityManager, SqliteLocalConnection } from "../src/data";
import type { em as protoEm } from "../src/data/prototype"; import type { em as protoEm } from "../src/data/prototype";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { slugify } from "core/utils/strings";
export function getDummyDatabase(memory: boolean = true): { export function getDummyDatabase(memory: boolean = true): {
dummyDb: SqliteDatabase; dummyDb: SqliteDatabase;
@@ -17,7 +20,7 @@ export function getDummyDatabase(memory: boolean = true): {
afterAllCleanup: async () => { afterAllCleanup: async () => {
if (!memory) await unlink(DB_NAME); if (!memory) await unlink(DB_NAME);
return true; return true;
} },
}; };
} }
@@ -27,7 +30,7 @@ export function getDummyConnection(memory: boolean = true) {
return { return {
dummyConnection, dummyConnection,
afterAllCleanup afterAllCleanup,
}; };
} }
@@ -39,7 +42,7 @@ type ConsoleSeverity = "log" | "warn" | "error";
const _oldConsoles = { const _oldConsoles = {
log: console.log, log: console.log,
warn: console.warn, warn: console.warn,
error: console.error error: console.error,
}; };
export function disableConsoleLog(severities: ConsoleSeverity[] = ["log", "warn"]) { export function disableConsoleLog(severities: ConsoleSeverity[] = ["log", "warn"]) {
@@ -71,3 +74,46 @@ export function schemaToEm(s: ReturnType<typeof protoEm>, conn?: Connection): En
export const assetsPath = `${import.meta.dir}/_assets`; export const assetsPath = `${import.meta.dir}/_assets`;
export const assetsTmpPath = `${import.meta.dir}/_assets/tmp`; export const assetsTmpPath = `${import.meta.dir}/_assets/tmp`;
export async function enableFetchLogging() {
const originalFetch = global.fetch;
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;
};
}

View File

@@ -16,25 +16,25 @@ const roles = {
"system.schema.read", "system.schema.read",
"system.access.api", "system.access.api",
"system.config.read", "system.config.read",
"data.entity.read" "data.entity.read",
], ],
is_default: true is_default: true,
}, },
admin: { admin: {
is_default: true, is_default: true,
implicit_allow: true implicit_allow: true,
} },
}, },
strict: { strict: {
guest: { guest: {
permissions: ["system.access.api", "system.config.read", "data.entity.read"], permissions: ["system.access.api", "system.config.read", "data.entity.read"],
is_default: true is_default: true,
}, },
admin: { admin: {
is_default: true, is_default: true,
implicit_allow: true implicit_allow: true,
} },
} },
}; };
const configs = { const configs = {
auth: { auth: {
@@ -42,31 +42,31 @@ const configs = {
entity_name: "users", entity_name: "users",
jwt: { jwt: {
secret: secureRandomString(20), secret: secureRandomString(20),
issuer: randomString(10) issuer: randomString(10),
}, },
roles: roles.strict, roles: roles.strict,
guard: { guard: {
enabled: true enabled: true,
} },
}, },
users: { users: {
normal: { normal: {
email: "normal@bknd.io", email: "normal@bknd.io",
password: "12345678" password: "12345678",
}, },
admin: { admin: {
email: "admin@bknd.io", email: "admin@bknd.io",
password: "12345678", password: "12345678",
role: "admin" role: "admin",
} },
} },
}; };
function createAuthApp() { function createAuthApp() {
const app = createApp({ const app = createApp({
initialConfig: { initialConfig: {
auth: configs.auth auth: configs.auth,
} },
}); });
app.emgr.onEvent( app.emgr.onEvent(
@@ -75,7 +75,7 @@ function createAuthApp() {
await app.createUser(configs.users.normal); await app.createUser(configs.users.normal);
await app.createUser(configs.users.admin); await app.createUser(configs.users.admin);
}, },
"sync" "sync",
); );
return app; return app;
@@ -94,14 +94,14 @@ const fns = <Mode extends "cookie" | "token" = "token">(app: App, mode?: Mode) =
if (mode === "cookie") { if (mode === "cookie") {
return { return {
cookie: `auth=${token};`, cookie: `auth=${token};`,
...additional ...additional,
}; };
} }
return { return {
Authorization: token ? `Bearer ${token}` : "", Authorization: token ? `Bearer ${token}` : "",
"Content-Type": "application/json", "Content-Type": "application/json",
...additional ...additional,
}; };
} }
function body(obj?: Record<string, any>) { function body(obj?: Record<string, any>) {
@@ -118,12 +118,12 @@ const fns = <Mode extends "cookie" | "token" = "token">(app: App, mode?: Mode) =
return { return {
login: async ( login: async (
user: any user: any,
): Promise<{ res: Response; data: Mode extends "token" ? AuthResponse : string }> => { ): Promise<{ res: Response; data: Mode extends "token" ? AuthResponse : string }> => {
const res = (await app.server.request("/api/auth/password/login", { const res = (await app.server.request("/api/auth/password/login", {
method: "POST", method: "POST",
headers: headers(), headers: headers(),
body: body(user) body: body(user),
})) as Response; })) as Response;
const data = mode === "cookie" ? getCookie(res, "auth") : await res.json(); const data = mode === "cookie" ? getCookie(res, "auth") : await res.json();
@@ -133,10 +133,10 @@ const fns = <Mode extends "cookie" | "token" = "token">(app: App, mode?: Mode) =
me: async (token?: string): Promise<Pick<AuthResponse, "user">> => { me: async (token?: string): Promise<Pick<AuthResponse, "user">> => {
const res = (await app.server.request("/api/auth/me", { const res = (await app.server.request("/api/auth/me", {
method: "GET", method: "GET",
headers: headers(token) headers: headers(token),
})) as Response; })) as Response;
return await res.json(); return await res.json();
} },
}; };
}; };
@@ -219,7 +219,7 @@ describe("integration auth", () => {
app.server.get("/get", auth(), async (c) => { app.server.get("/get", auth(), async (c) => {
return c.json({ return c.json({
user: c.get("auth").user ?? null user: c.get("auth").user ?? null,
}); });
}); });
app.server.get("/wait", auth(), async (c) => { app.server.get("/wait", auth(), async (c) => {
@@ -232,7 +232,7 @@ describe("integration auth", () => {
expect(me.user.email).toBe(configs.users.normal.email); expect(me.user.email).toBe(configs.users.normal.email);
app.server.request("/wait", { app.server.request("/wait", {
headers: { Authorization: `Bearer ${data.token}` } headers: { Authorization: `Bearer ${data.token}` },
}); });
{ {

View File

@@ -8,7 +8,7 @@ describe("integration config", () => {
await app.build(); await app.build();
const api = new Api({ const api = new Api({
host: "http://localhost", host: "http://localhost",
fetcher: app.server.request as typeof fetch fetcher: app.server.request as typeof fetch,
}); });
// create entity // create entity
@@ -16,7 +16,7 @@ describe("integration config", () => {
name: "posts", name: "posts",
config: { sort_field: "id", sort_dir: "asc" }, config: { sort_field: "id", sort_dir: "asc" },
fields: { id: { type: "primary", name: "id" }, asdf: { type: "text" } }, fields: { id: { type: "primary", name: "id" }, asdf: { type: "text" } },
type: "regular" type: "regular",
}); });
expect(app.em.entities.map((e) => e.name)).toContain("posts"); expect(app.em.entities.map((e) => e.name)).toContain("posts");

View File

@@ -2,9 +2,9 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { createApp, registries } from "../../src"; import { createApp, registries } from "../../src";
import { StorageLocalAdapter } from "../../src/adapter/node";
import { mergeObject, randomString } from "../../src/core/utils"; import { mergeObject, randomString } from "../../src/core/utils";
import type { TAppMediaConfig } from "../../src/media/media-schema"; import type { TAppMediaConfig } from "../../src/media/media-schema";
import { StorageLocalAdapter } from "../../src/media/storage/adapters/StorageLocalAdapter";
import { assetsPath, assetsTmpPath, disableConsoleLog, enableConsoleLog } from "../helper"; import { assetsPath, assetsTmpPath, disableConsoleLog, enableConsoleLog } from "../helper";
beforeAll(() => { beforeAll(() => {
@@ -22,13 +22,13 @@ async function makeApp(mediaOverride: Partial<TAppMediaConfig> = {}) {
adapter: { adapter: {
type: "local", type: "local",
config: { config: {
path: assetsTmpPath path: assetsTmpPath,
} },
} },
}, },
mediaOverride mediaOverride,
) ),
} },
}); });
await app.build(); await app.build();
@@ -43,16 +43,17 @@ beforeAll(disableConsoleLog);
afterAll(enableConsoleLog); afterAll(enableConsoleLog);
describe("MediaController", () => { describe("MediaController", () => {
test("accepts direct", async () => { test.only("accepts direct", async () => {
const app = await makeApp(); const app = await makeApp();
const file = Bun.file(path); const file = Bun.file(path);
const name = makeName("png"); const name = makeName("png");
const res = await app.server.request("/api/media/upload/" + name, { const res = await app.server.request("/api/media/upload/" + name, {
method: "POST", method: "POST",
body: file body: file,
}); });
const result = (await res.json()) as any; const result = (await res.json()) as any;
console.log(result);
expect(result.name).toBe(name); expect(result.name).toBe(name);
const destFile = Bun.file(assetsTmpPath + "/" + name); const destFile = Bun.file(assetsTmpPath + "/" + name);
@@ -70,7 +71,7 @@ describe("MediaController", () => {
const res = await app.server.request("/api/media/upload/" + name, { const res = await app.server.request("/api/media/upload/" + name, {
method: "POST", method: "POST",
body: form body: form,
}); });
const result = (await res.json()) as any; const result = (await res.json()) as any;
expect(result.name).toBe(name); expect(result.name).toBe(name);
@@ -87,7 +88,7 @@ describe("MediaController", () => {
const name = makeName("png"); const name = makeName("png");
const res = await app.server.request("/api/media/upload/" + name, { const res = await app.server.request("/api/media/upload/" + name, {
method: "POST", method: "POST",
body: file body: file,
}); });
expect(res.status).toBe(413); expect(res.status).toBe(413);

View File

@@ -60,7 +60,7 @@ describe("Storage", async () => {
test("uploads a file", async () => { test("uploads a file", async () => {
const { const {
meta: { type, size } meta: { type, size },
} = await storage.uploadFile("hello", "world.txt"); } = await storage.uploadFile("hello", "world.txt");
expect({ type, size }).toEqual({ type: "text/plain", size: 0 }); expect({ type, size }).toEqual({ type: "text/plain", size: 0 });
}); });

View File

@@ -14,7 +14,7 @@ test("what", async () => {
const mf = new Miniflare({ const mf = new Miniflare({
modules: true, modules: true,
script: "export default { async fetch() { return new Response(null); } }", script: "export default { async fetch() { return new Response(null); } }",
r2Buckets: ["BUCKET"] r2Buckets: ["BUCKET"],
}); });
const bucket = await mf.getR2Bucket("BUCKET"); const bucket = await mf.getR2Bucket("BUCKET");

View File

@@ -8,17 +8,19 @@ const {
CLOUDINARY_CLOUD_NAME, CLOUDINARY_CLOUD_NAME,
CLOUDINARY_API_KEY, CLOUDINARY_API_KEY,
CLOUDINARY_API_SECRET, CLOUDINARY_API_SECRET,
CLOUDINARY_UPLOAD_PRESET CLOUDINARY_UPLOAD_PRESET,
} = dotenvOutput.parsed!; } = dotenvOutput.parsed!;
const ALL_TESTS = !!process.env.ALL_TESTS; const ALL_TESTS = !!process.env.ALL_TESTS;
describe.skipIf(ALL_TESTS)("StorageCloudinaryAdapter", () => { describe.skipIf(ALL_TESTS)("StorageCloudinaryAdapter", () => {
if (ALL_TESTS) return;
const adapter = new StorageCloudinaryAdapter({ const adapter = new StorageCloudinaryAdapter({
cloud_name: CLOUDINARY_CLOUD_NAME as string, cloud_name: CLOUDINARY_CLOUD_NAME as string,
api_key: CLOUDINARY_API_KEY as string, api_key: CLOUDINARY_API_KEY as string,
api_secret: CLOUDINARY_API_SECRET as string, api_secret: CLOUDINARY_API_SECRET as string,
upload_preset: CLOUDINARY_UPLOAD_PRESET as string upload_preset: CLOUDINARY_UPLOAD_PRESET as string,
}); });
const file = Bun.file(`${import.meta.dir}/icon.png`); const file = Bun.file(`${import.meta.dir}/icon.png`);

View File

@@ -1,13 +1,14 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { randomString } from "../../../src/core/utils"; import { randomString } from "../../../src/core/utils";
import { StorageLocalAdapter } from "../../../src/media/storage/adapters/StorageLocalAdapter"; import { StorageLocalAdapter } from "../../../src/media/storage/adapters/StorageLocalAdapter";
import { assetsPath, assetsTmpPath } from "../../helper";
describe("StorageLocalAdapter", () => { describe("StorageLocalAdapter", () => {
const adapter = new StorageLocalAdapter({ const adapter = new StorageLocalAdapter({
path: `${import.meta.dir}/local` path: assetsTmpPath,
}); });
const file = Bun.file(`${import.meta.dir}/icon.png`); const file = Bun.file(`${assetsPath}/image.png`);
const _filename = randomString(10); const _filename = randomString(10);
const filename = `${_filename}.png`; const filename = `${_filename}.png`;
@@ -35,7 +36,7 @@ describe("StorageLocalAdapter", () => {
test("gets object meta", async () => { test("gets object meta", async () => {
expect(await adapter.getObjectMeta(filename)).toEqual({ expect(await adapter.getObjectMeta(filename)).toEqual({
type: file.type, // image/png type: file.type, // image/png
size: file.size size: file.size,
}); });
}); });

View File

@@ -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 { randomString } from "../../../src/core/utils";
import { StorageS3Adapter } from "../../../src/media"; import { StorageS3Adapter } from "../../../src/media";
import { config } from "dotenv"; import { config } from "dotenv";
//import { enableFetchLogging } from "../../helper";
const dotenvOutput = config({ path: `${import.meta.dir}/../../../.env` }); 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 } = const { R2_ACCESS_KEY, R2_SECRET_ACCESS_KEY, R2_URL, AWS_ACCESS_KEY, AWS_SECRET_KEY, AWS_S3_URL } =
dotenvOutput.parsed!; dotenvOutput.parsed!;
// @todo: mock r2/s3 responses for faster tests // @todo: mock r2/s3 responses for faster tests
const ALL_TESTS = !!process.env.ALL_TESTS; const ALL_TESTS = !!process.env.ALL_TESTS;
console.log("ALL_TESTS?", ALL_TESTS);
/*
// @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 () => { describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => {
console.log("ALL_TESTS", process.env.ALL_TESTS); if (ALL_TESTS) return;
const versions = [ const versions = [
[ [
"r2", "r2",
new StorageS3Adapter({ new StorageS3Adapter({
access_key: R2_ACCESS_KEY as string, access_key: R2_ACCESS_KEY as string,
secret_access_key: R2_SECRET_ACCESS_KEY as string, secret_access_key: R2_SECRET_ACCESS_KEY as string,
url: R2_URL as string url: R2_URL as string,
}) }),
], ],
[ [
"s3", "s3",
new StorageS3Adapter({ new StorageS3Adapter({
access_key: AWS_ACCESS_KEY as string, access_key: AWS_ACCESS_KEY as string,
secret_access_key: AWS_SECRET_KEY as string, secret_access_key: AWS_SECRET_KEY as string,
url: AWS_S3_URL as string url: AWS_S3_URL as string,
}) }),
] ],
] as const; ] as const;
const _conf = { const _conf = {
@@ -39,8 +52,8 @@ describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => {
"objectExists", "objectExists",
"getObject", "getObject",
"deleteObject", "deleteObject",
"getObjectMeta" "getObjectMeta",
] ],
}; };
const file = Bun.file(`${import.meta.dir}/icon.png`); const file = Bun.file(`${import.meta.dir}/icon.png`);
@@ -55,7 +68,7 @@ describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => {
// @todo: add mocked fetch for faster tests // @todo: add mocked fetch for faster tests
describe.each(versions)("StorageS3Adapter for %s", async (name, adapter) => { describe.each(versions)("StorageS3Adapter for %s", async (name, adapter) => {
if (!_conf.adapters.includes(name)) { if (!_conf.adapters.includes(name) || ALL_TESTS) {
console.log("Skipping", name); console.log("Skipping", name);
return; return;
} }
@@ -64,7 +77,7 @@ describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => {
test.skipIf(disabled("putObject"))("puts an object", async () => { test.skipIf(disabled("putObject"))("puts an object", async () => {
objects = (await adapter.listObjects()).length; objects = (await adapter.listObjects()).length;
expect(await adapter.putObject(filename, file)).toBeString(); expect(await adapter.putObject(filename, file as any)).toBeString();
}); });
test.skipIf(disabled("listObjects"))("lists objects", async () => { 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 () => { test.skipIf(disabled("getObjectMeta"))("gets object meta", async () => {
expect(await adapter.getObjectMeta(filename)).toEqual({ expect(await adapter.getObjectMeta(filename)).toEqual({
type: file.type, // image/png type: file.type, // image/png
size: file.size size: file.size,
}); });
}); });

View File

@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import * as large from "../../src/media/storage/mime-types"; import * as large from "../../src/media/storage/mime-types";
import * as tiny from "../../src/media/storage/mime-types-tiny"; import * as tiny from "../../src/media/storage/mime-types-tiny";
import { getRandomizedFilename } from "../../src/media/utils";
describe("media/mime-types", () => { describe("media/mime-types", () => {
test("tiny resolves", () => { test("tiny resolves", () => {
@@ -27,7 +28,7 @@ describe("media/mime-types", () => {
exts, exts,
ext, ext,
expected: ex, expected: ex,
actual: large.guessMimeType("." + ext) actual: large.guessMimeType("." + ext),
}); });
throw new Error(`Failed for ${ext}`); throw new Error(`Failed for ${ext}`);
} }
@@ -36,19 +37,62 @@ describe("media/mime-types", () => {
}); });
test("isMimeType", () => { test("isMimeType", () => {
expect(tiny.isMimeType("image/jpeg")).toBe(true); const tests = [
expect(tiny.isMimeType("image/jpeg", ["image/png"])).toBe(true); ["image/avif", true],
expect(tiny.isMimeType("image/png", ["image/png"])).toBe(false); ["image/AVIF", true],
expect(tiny.isMimeType("image/png")).toBe(true); ["image/jpeg", true],
expect(tiny.isMimeType("whatever")).toBe(false); ["image/jpeg", true, ["image/png"]],
expect(tiny.isMimeType("text/tab-separated-values")).toBe(true); ["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", () => { test("extension", () => {
expect(tiny.extension("image/png")).toBe("png"); const tests = [
expect(tiny.extension("image/jpeg")).toBe("jpeg"); ["image/avif", "avif"],
expect(tiny.extension("application/zip")).toBe("zip"); ["image/png", "png"],
expect(tiny.extension("text/tab-separated-values")).toBe("tsv"); ["image/PNG", "png"],
expect(tiny.extension("application/zip")).toBe("zip"); ["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);
}
}); });
}); });

View File

@@ -43,10 +43,10 @@ describe("AppAuth", () => {
{ {
enabled: true, enabled: true,
jwt: { jwt: {
secret: "123456" secret: "123456",
} },
}, },
ctx ctx,
); );
await auth.build(); await auth.build();
@@ -63,12 +63,12 @@ describe("AppAuth", () => {
const res = await app.request("/password/register", { const res = await app.request("/password/register", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
email: "some@body.com", email: "some@body.com",
password: "123456" password: "123456",
}) }),
}); });
enableConsoleLog(); enableConsoleLog();
expect(res.status).toBe(200); expect(res.status).toBe(200);
@@ -85,10 +85,10 @@ describe("AppAuth", () => {
auth: { auth: {
enabled: true, enabled: true,
jwt: { jwt: {
secret: "123456" secret: "123456",
} },
} },
} },
}); });
await app.build(); await app.build();
@@ -109,14 +109,14 @@ describe("AppAuth", () => {
initialConfig: { initialConfig: {
auth: { auth: {
entity_name: "users", entity_name: "users",
enabled: true enabled: true,
}, },
data: em({ data: em({
users: entity("users", { users: entity("users", {
additional: text() additional: text(),
}) }),
}).toJSON() }).toJSON(),
} },
}); });
await app.build(); await app.build();
@@ -132,21 +132,21 @@ describe("AppAuth", () => {
const app = createApp({ const app = createApp({
initialConfig: { initialConfig: {
auth: { auth: {
enabled: true enabled: true,
}, },
data: em({ data: em({
users: entity("users", { users: entity("users", {
strategy: text({ strategy: text({
fillable: true, fillable: true,
hidden: false hidden: false,
}), }),
strategy_value: text({ strategy_value: text({
fillable: true, fillable: true,
hidden: false hidden: false,
}) }),
}) }),
}).toJSON() }).toJSON(),
} },
}); });
await app.build(); await app.build();
@@ -157,7 +157,7 @@ describe("AppAuth", () => {
const authField = make(name, _authFieldProto as any); const authField = make(name, _authFieldProto as any);
const field = users.field(name)!; const field = users.field(name)!;
for (const prop of props) { for (const prop of props) {
expect(field.config[prop]).toBe(authField.config[prop]); expect(field.config[prop]).toEqual(authField.config[prop]);
} }
} }
}); });

View File

@@ -19,16 +19,16 @@ describe("AppMedia", () => {
adapter: { adapter: {
type: "local", type: "local",
config: { config: {
path: "./" path: "./",
} },
} },
}, },
data: em({ data: em({
media: entity("media", { media: entity("media", {
additional: text() additional: text(),
}) }),
}).toJSON() }).toJSON(),
} },
}); });
await app.build(); await app.build();
@@ -49,7 +49,7 @@ describe("AppMedia", () => {
"modified_at", "modified_at",
"reference", "reference",
"entity_id", "entity_id",
"metadata" "metadata",
]); ]);
}); });
}); });

View File

@@ -47,7 +47,7 @@ describe("Module", async () => {
prt = { prt = {
ensureEntity: this.ensureEntity.bind(this), ensureEntity: this.ensureEntity.bind(this),
ensureIndex: this.ensureIndex.bind(this), ensureIndex: this.ensureIndex.bind(this),
ensureSchema: this.ensureSchema.bind(this) ensureSchema: this.ensureSchema.bind(this),
}; };
get em() { get em() {
@@ -60,7 +60,7 @@ describe("Module", async () => {
Object.values(_em.entities), Object.values(_em.entities),
new DummyConnection(), new DummyConnection(),
_em.relations, _em.relations,
_em.indices _em.indices,
); );
return new M({} as any, { em, flags: Module.ctx_flags } as any); return new M({} as any, { em, flags: Module.ctx_flags } as any);
} }
@@ -69,14 +69,14 @@ describe("Module", async () => {
entities: _em.entities.map((e) => ({ entities: _em.entities.map((e) => ({
name: e.name, name: e.name,
fields: e.fields.map((f) => f.name), fields: e.fields.map((f) => f.name),
type: e.type type: e.type,
})), })),
indices: _em.indices.map((i) => ({ indices: _em.indices.map((i) => ({
name: i.name, name: i.name,
entity: i.entity.name, entity: i.entity.name,
fields: i.fields.map((f) => f.name), fields: i.fields.map((f) => f.name),
unique: i.unique unique: i.unique,
})) })),
}; };
} }
@@ -88,15 +88,15 @@ describe("Module", async () => {
expect(flat(make(initial).em)).toEqual({ expect(flat(make(initial).em)).toEqual({
entities: [], entities: [],
indices: [] indices: [],
}); });
}); });
test("init", () => { test("init", () => {
const initial = em({ const initial = em({
users: entity("u", { users: entity("u", {
name: text() name: text(),
}) }),
}); });
const m = make(initial); const m = make(initial);
@@ -107,18 +107,18 @@ describe("Module", async () => {
{ {
name: "u", name: "u",
fields: ["id", "name"], fields: ["id", "name"],
type: "regular" type: "regular",
} },
], ],
indices: [] indices: [],
}); });
}); });
test("ensure entity", () => { test("ensure entity", () => {
const initial = em({ const initial = em({
users: entity("u", { users: entity("u", {
name: text() name: text(),
}) }),
}); });
const m = make(initial); const m = make(initial);
@@ -127,17 +127,17 @@ describe("Module", async () => {
{ {
name: "u", name: "u",
fields: ["id", "name"], fields: ["id", "name"],
type: "regular" type: "regular",
} },
], ],
indices: [] indices: [],
}); });
// this should add a new entity // this should add a new entity
m.prt.ensureEntity( m.prt.ensureEntity(
entity("p", { entity("p", {
title: text() title: text(),
}) }),
); );
// this should only add the field "important" // this should only add the field "important"
@@ -145,11 +145,11 @@ describe("Module", async () => {
entity( entity(
"u", "u",
{ {
important: text() important: text(),
}, },
undefined, undefined,
"system" "system",
) ),
); );
expect(m.ctx.flags.sync_required).toBe(true); expect(m.ctx.flags.sync_required).toBe(true);
@@ -159,22 +159,22 @@ describe("Module", async () => {
name: "u", name: "u",
fields: ["id", "name", "important"], fields: ["id", "name", "important"],
// ensured type must be present // ensured type must be present
type: "system" type: "system",
}, },
{ {
name: "p", name: "p",
fields: ["id", "title"], fields: ["id", "title"],
type: "regular" type: "regular",
} },
], ],
indices: [] indices: [],
}); });
}); });
test("ensure index", () => { test("ensure index", () => {
const users = entity("u", { const users = entity("u", {
name: text(), name: text(),
title: text() title: text(),
}); });
const initial = em({ users }, ({ index }, { users }) => { const initial = em({ users }, ({ index }, { users }) => {
index(users).on(["title"]); index(users).on(["title"]);
@@ -189,23 +189,23 @@ describe("Module", async () => {
{ {
name: "u", name: "u",
fields: ["id", "name", "title"], fields: ["id", "name", "title"],
type: "regular" type: "regular",
} },
], ],
indices: [ indices: [
{ {
name: "idx_u_title", name: "idx_u_title",
entity: "u", entity: "u",
fields: ["title"], fields: ["title"],
unique: false unique: false,
}, },
{ {
name: "idx_u_name", name: "idx_u_name",
entity: "u", entity: "u",
fields: ["name"], fields: ["name"],
unique: false unique: false,
} },
] ],
}); });
}); });
}); });

View File

@@ -39,10 +39,10 @@ describe("ModuleManager", async () => {
basepath: "/api/data2", basepath: "/api/data2",
entities: { entities: {
test: entity("test", { test: entity("test", {
content: text() content: text(),
}).toJSON() }).toJSON(),
} },
} },
}); });
//const { version, ...json } = mm.toJSON() as any; //const { version, ...json } = mm.toJSON() as any;
@@ -69,10 +69,10 @@ describe("ModuleManager", async () => {
basepath: "/api/data2", basepath: "/api/data2",
entities: { entities: {
test: entity("test", { test: entity("test", {
content: text() content: text(),
}).toJSON() }).toJSON(),
} },
} },
}; };
//const { version, ...json } = mm.toJSON() as any; //const { version, ...json } = mm.toJSON() as any;
@@ -105,7 +105,7 @@ describe("ModuleManager", async () => {
const c2 = getDummyConnection(); const c2 = getDummyConnection();
const db = c2.dummyConnection.kysely; const db = c2.dummyConnection.kysely;
const mm2 = new ModuleManager(c2.dummyConnection, { const mm2 = new ModuleManager(c2.dummyConnection, {
initial: { version: version - 1, ...json } initial: { version: version - 1, ...json },
}); });
await mm2.syncConfigTable(); await mm2.syncConfigTable();
@@ -129,7 +129,7 @@ describe("ModuleManager", async () => {
const db = c2.dummyConnection.kysely; const db = c2.dummyConnection.kysely;
const mm2 = new ModuleManager(c2.dummyConnection, { const mm2 = new ModuleManager(c2.dummyConnection, {
initial: { version: version - 1, ...json } initial: { version: version - 1, ...json },
}); });
await mm2.syncConfigTable(); await mm2.syncConfigTable();
await db await db
@@ -157,8 +157,8 @@ describe("ModuleManager", async () => {
...json, ...json,
data: { data: {
...json.data, ...json.data,
basepath: "/api/data2" basepath: "/api/data2",
} },
}; };
await db await db
.insertInto(TABLE_NAME) .insertInto(TABLE_NAME)
@@ -190,9 +190,9 @@ describe("ModuleManager", async () => {
...configs.server, ...configs.server,
admin: { admin: {
...configs.server.admin, ...configs.server.admin,
color_scheme: "dark" color_scheme: "dark",
} },
} },
}); });
}); });
@@ -201,11 +201,11 @@ describe("ModuleManager", async () => {
const partial = { const partial = {
auth: { auth: {
enabled: true enabled: true,
} },
}; };
const mm = new ModuleManager(dummyConnection, { const mm = new ModuleManager(dummyConnection, {
initial: partial initial: partial,
}); });
await mm.build(); await mm.build();
@@ -227,9 +227,9 @@ describe("ModuleManager", async () => {
const mm2 = new ModuleManager(c2.dummyConnection, { const mm2 = new ModuleManager(c2.dummyConnection, {
initial: { initial: {
auth: { auth: {
basepath: "/shouldnt/take/this" basepath: "/shouldnt/take/this",
} },
} },
}); });
await mm2.syncConfigTable(); await mm2.syncConfigTable();
const payload = { const payload = {
@@ -237,15 +237,15 @@ describe("ModuleManager", async () => {
auth: { auth: {
...json.auth, ...json.auth,
enabled: true, enabled: true,
basepath: "/api/auth2" basepath: "/api/auth2",
} },
}; };
await db await db
.insertInto(TABLE_NAME) .insertInto(TABLE_NAME)
.values({ .values({
type: "config", type: "config",
json: JSON.stringify(payload), json: JSON.stringify(payload),
version: CURRENT_VERSION version: CURRENT_VERSION,
}) })
.execute(); .execute();
await mm2.build(); await mm2.build();
@@ -256,7 +256,7 @@ describe("ModuleManager", async () => {
describe("revert", async () => { describe("revert", async () => {
const failingModuleSchema = Type.Object({ const failingModuleSchema = Type.Object({
value: Type.Optional(Type.Number()) value: Type.Optional(Type.Number()),
}); });
class FailingModule extends Module<typeof failingModuleSchema> { class FailingModule extends Module<typeof failingModuleSchema> {
getSchema() { getSchema() {
@@ -301,8 +301,8 @@ describe("ModuleManager", async () => {
const mm = new TestModuleManager(dummyConnection, { const mm = new TestModuleManager(dummyConnection, {
initial: { initial: {
// @ts-ignore // @ts-ignore
failing: { value: 2 } failing: { value: 2 },
} },
}); });
await mm.build(); await mm.build();
expect(mm.configs()["failing"].value).toBe(2); expect(mm.configs()["failing"].value).toBe(2);
@@ -313,8 +313,8 @@ describe("ModuleManager", async () => {
const mm = new TestModuleManager(dummyConnection, { const mm = new TestModuleManager(dummyConnection, {
initial: { initial: {
// @ts-ignore // @ts-ignore
failing: { value: -1 } failing: { value: -1 },
} },
}); });
expect(mm.build()).rejects.toThrow(/value must be positive/); expect(mm.build()).rejects.toThrow(/value must be positive/);
expect(mm.configs()["failing"].value).toBe(-1); expect(mm.configs()["failing"].value).toBe(-1);
@@ -326,7 +326,7 @@ describe("ModuleManager", async () => {
const mm = new TestModuleManager(dummyConnection, { const mm = new TestModuleManager(dummyConnection, {
onUpdated: async () => { onUpdated: async () => {
mockOnUpdated(); mockOnUpdated();
} },
}); });
await mm.build(); await mm.build();
// @ts-ignore // @ts-ignore
@@ -342,11 +342,11 @@ describe("ModuleManager", async () => {
const mm = new TestModuleManager(dummyConnection, { const mm = new TestModuleManager(dummyConnection, {
initial: { initial: {
// @ts-ignore // @ts-ignore
failing: { value: 1 } failing: { value: 1 },
}, },
onUpdated: async () => { onUpdated: async () => {
mockOnUpdated(); mockOnUpdated();
} },
}); });
await mm.build(); await mm.build();
expect(mm.configs()["failing"].value).toBe(1); expect(mm.configs()["failing"].value).toBe(1);
@@ -354,7 +354,7 @@ describe("ModuleManager", async () => {
// now safe mutate // now safe mutate
// @ts-ignore // @ts-ignore
expect(mm.mutateConfigSafe("failing").set({ value: -2 })).rejects.toThrow( 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(mm.configs()["failing"].value).toBe(1);
expect(mockOnUpdated).toHaveBeenCalled(); expect(mockOnUpdated).toHaveBeenCalled();

View 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");
});
});

View 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": {}
}
}

View File

@@ -19,7 +19,7 @@ export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildCon
guard: new Guard(), guard: new Guard(),
flags: Module.ctx_flags, flags: Module.ctx_flags,
logger: new DebugLogger(false), logger: new DebugLogger(false),
...overrides ...overrides,
}; };
} }

View File

@@ -16,7 +16,7 @@ describe("json form", () => {
["0", { type: "boolean" }, false], ["0", { type: "boolean" }, false],
["on", { type: "boolean" }, true], ["on", { type: "boolean" }, true],
["off", { type: "boolean" }, false], ["off", { type: "boolean" }, false],
["null", { type: "null" }, null] ["null", { type: "null" }, null],
] satisfies [string, Exclude<JSONSchema, boolean>, any][]; ] satisfies [string, Exclude<JSONSchema, boolean>, any][];
for (const [input, schema, output] of examples) { for (const [input, schema, output] of examples) {
@@ -35,7 +35,7 @@ describe("json form", () => {
["array", "array", true], ["array", "array", true],
["object", "array", false], ["object", "array", false],
[["string", "number"], "number", true], [["string", "number"], "number", true],
["number", ["string", "number"], true] ["number", ["string", "number"], true],
] satisfies [IsTypeType, IsTypeType, boolean][]; ] satisfies [IsTypeType, IsTypeType, boolean][];
for (const [type, schemaType, output] of examples) { for (const [type, schemaType, output] of examples) {
@@ -48,7 +48,7 @@ describe("json form", () => {
["#/nested/property/0/name", "#/nested/property/0"], ["#/nested/property/0/name", "#/nested/property/0"],
["#/nested/property/0", "#/nested/property"], ["#/nested/property/0", "#/nested/property"],
["#/nested/property", "#/nested"], ["#/nested/property", "#/nested"],
["#/nested", "#"] ["#/nested", "#"],
]; ];
for (const [input, output] of examples) { for (const [input, output] of examples) {
@@ -61,16 +61,16 @@ describe("json form", () => {
[ [
"#/description", "#/description",
{ type: "object", properties: { description: { type: "string" } } }, { type: "object", properties: { description: { type: "string" } } },
false false,
], ],
[ [
"#/description", "#/description",
{ {
type: "object", type: "object",
required: ["description"], required: ["description"],
properties: { description: { type: "string" } } properties: { description: { type: "string" } },
}, },
true true,
], ],
[ [
"#/nested/property", "#/nested/property",
@@ -79,11 +79,11 @@ describe("json form", () => {
properties: { properties: {
nested: { nested: {
type: "object", type: "object",
properties: { property: { type: "string" } } properties: { property: { type: "string" } },
} },
} },
}, },
false false,
], ],
[ [
"#/nested/property", "#/nested/property",
@@ -93,12 +93,12 @@ describe("json form", () => {
nested: { nested: {
type: "object", type: "object",
required: ["property"], required: ["property"],
properties: { property: { type: "string" } } properties: { property: { type: "string" } },
} },
} },
}, },
true true,
] ],
] satisfies [string, Exclude<JSONSchema, boolean>, boolean][]; ] satisfies [string, Exclude<JSONSchema, boolean>, boolean][];
for (const [pointer, schema, output] of examples) { for (const [pointer, schema, output] of examples) {
@@ -113,7 +113,7 @@ describe("json form", () => {
["tags", "0", "0.tags"], ["tags", "0", "0.tags"],
["tags", 0, "0.tags"], ["tags", 0, "0.tags"],
["nested.property", "prefix", "prefix.nested.property"], ["nested.property", "prefix", "prefix.nested.property"],
["nested.property", "", "nested.property"] ["nested.property", "", "nested.property"],
] satisfies [string, any, string][]; ] satisfies [string, any, string][];
for (const [path, prefix, output] of examples) { for (const [path, prefix, output] of examples) {
@@ -128,7 +128,7 @@ describe("json form", () => {
["tags", "0", "tags.0"], ["tags", "0", "tags.0"],
["tags", 0, "tags.0"], ["tags", 0, "tags.0"],
["nested.property", "suffix", "nested.property.suffix"], ["nested.property", "suffix", "nested.property.suffix"],
["nested.property", "", "nested.property"] ["nested.property", "", "nested.property"],
] satisfies [string, any, string][]; ] satisfies [string, any, string][];
for (const [path, suffix, output] of examples) { for (const [path, suffix, output] of examples) {

View File

@@ -1,13 +0,0 @@
//import type { BkndConfig } from "./src";
export default {
app: {
connection: {
type: "libsql",
config: {
//url: "http://localhost:8080"
url: ":memory:"
}
}
}
};

View File

@@ -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()]);
}

View File

@@ -27,9 +27,9 @@ function buildTypes() {
onExit: () => { onExit: () => {
console.log("Types aliased"); console.log("Types aliased");
types_running = false; types_running = false;
} },
}); });
} },
}); });
} }
@@ -46,41 +46,52 @@ if (types && !watch) {
buildTypes(); buildTypes();
} }
function banner(title: string) {
console.log("");
console.log("=".repeat(40));
console.log(title.toUpperCase());
console.log("-".repeat(40));
}
/** /**
* Building backend and general API * Building backend and general API
*/ */
async function buildApi() { async function buildApi() {
banner("Building API");
await tsup.build({ await tsup.build({
minify, minify,
sourcemap, sourcemap,
watch, watch,
entry: ["src/index.ts", "src/data/index.ts", "src/core/index.ts", "src/core/utils/index.ts"], entry: ["src/index.ts", "src/data/index.ts", "src/core/index.ts", "src/core/utils/index.ts"],
outDir: "dist", outDir: "dist",
external: ["bun:test", "@libsql/client", "bknd/client"], external: ["bun:test", "@libsql/client"],
metafile: true, metafile: true,
platform: "browser", platform: "browser",
format: ["esm"], format: ["esm"],
splitting: false, splitting: false,
treeshake: true, treeshake: true,
loader: { loader: {
".svg": "dataurl" ".svg": "dataurl",
}, },
onSuccess: async () => { onSuccess: async () => {
delayTypes(); delayTypes();
} },
}); });
} }
async function rewriteClient(path: string) {
const bundle = await Bun.file(path).text();
await Bun.write(path, '"use client";\n' + bundle.replaceAll("ui/client", "bknd/client"));
}
/** /**
* Building UI for direct imports * Building UI for direct imports
*/ */
async function buildUi() { async function buildUi() {
await tsup.build({ const base = {
minify, minify,
sourcemap, sourcemap,
watch, watch,
entry: ["src/ui/index.ts", "src/ui/client/index.ts", "src/ui/main.css", "src/ui/styles.css"],
outDir: "dist/ui",
external: [ external: [
"bun:test", "bun:test",
"react", "react",
@@ -90,7 +101,7 @@ async function buildUi() {
"use-sync-external-store", "use-sync-external-store",
/codemirror/, /codemirror/,
"@xyflow/react", "@xyflow/react",
"@mantine/core" "@mantine/core",
], ],
metafile: true, metafile: true,
platform: "browser", platform: "browser",
@@ -99,14 +110,33 @@ async function buildUi() {
bundle: true, bundle: true,
treeshake: true, treeshake: true,
loader: { loader: {
".svg": "dataurl" ".svg": "dataurl",
}, },
esbuildOptions: (options) => { esbuildOptions: (options) => {
options.logLevel = "silent"; options.logLevel = "silent";
}, },
} satisfies tsup.Options;
banner("Building UI");
await tsup.build({
...base,
entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"],
outDir: "dist/ui",
onSuccess: async () => { onSuccess: async () => {
await rewriteClient("./dist/ui/index.js");
delayTypes(); delayTypes();
} },
});
banner("Building Client");
await tsup.build({
...base,
entry: ["src/ui/client/index.ts"],
outDir: "dist/ui/client",
onSuccess: async () => {
await rewriteClient("./dist/ui/client/index.js");
delayTypes();
},
}); });
} }
@@ -116,6 +146,7 @@ async function buildUi() {
* - ui/client is external, and after built replaced with "bknd/client" * - ui/client is external, and after built replaced with "bknd/client"
*/ */
async function buildUiElements() { async function buildUiElements() {
banner("Building UI Elements");
await tsup.build({ await tsup.build({
minify, minify,
sourcemap, sourcemap,
@@ -128,7 +159,7 @@ async function buildUiElements() {
"react-dom", "react-dom",
"react/jsx-runtime", "react/jsx-runtime",
"react/jsx-dev-runtime", "react/jsx-dev-runtime",
"use-sync-external-store" "use-sync-external-store",
], ],
metafile: true, metafile: true,
platform: "browser", platform: "browser",
@@ -137,22 +168,18 @@ async function buildUiElements() {
bundle: true, bundle: true,
treeshake: true, treeshake: true,
loader: { loader: {
".svg": "dataurl" ".svg": "dataurl",
}, },
esbuildOptions: (options) => { esbuildOptions: (options) => {
options.alias = { options.alias = {
// not important for elements, mock to reduce bundle // not important for elements, mock to reduce bundle
"tailwind-merge": "./src/ui/elements/mocks/tailwind-merge.ts" "tailwind-merge": "./src/ui/elements/mocks/tailwind-merge.ts",
}; };
}, },
onSuccess: async () => { onSuccess: async () => {
// manually replace ui/client with bknd/client await rewriteClient("./dist/ui/elements/index.js");
const path = "./dist/ui/elements/index.js";
const bundle = await Bun.file(path).text();
await Bun.write(path, bundle.replaceAll("ui/client", "bknd/client"));
delayTypes(); delayTypes();
} },
}); });
} }
@@ -176,49 +203,51 @@ function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsu
...overrides, ...overrides,
define: { define: {
__isDev: "0", __isDev: "0",
...overrides.define ...overrides.define,
}, },
external: [ external: [
/^cloudflare*/, /^cloudflare*/,
/^@?(hono|libsql).*?/, /^@?(hono|libsql).*?/,
/^(bknd|react|next|node).*?/, /^(bknd|react|next|node).*?/,
/.*\.(html)$/, /.*\.(html)$/,
...(Array.isArray(overrides.external) ? overrides.external : []) ...(Array.isArray(overrides.external) ? overrides.external : []),
] ],
}; };
} }
async function buildAdapters() { async function buildAdapters() {
banner("Building Adapters");
// base adapter handles // base adapter handles
await tsup.build({ await tsup.build({
...baseConfig(""), ...baseConfig(""),
entry: ["src/adapter/index.ts"], entry: ["src/adapter/index.ts"],
outDir: "dist/adapter" outDir: "dist/adapter",
}); });
// specific adatpers // specific adatpers
await tsup.build(baseConfig("remix")); await tsup.build(baseConfig("remix"));
await tsup.build(baseConfig("bun")); await tsup.build(baseConfig("bun"));
await tsup.build(baseConfig("astro")); await tsup.build(baseConfig("astro"));
await tsup.build(baseConfig("aws"));
await tsup.build( await tsup.build(
baseConfig("cloudflare", { baseConfig("cloudflare", {
external: [/^kysely/] external: [/^kysely/],
}) }),
); );
await tsup.build({ await tsup.build({
...baseConfig("vite"), ...baseConfig("vite"),
platform: "node" platform: "node",
}); });
await tsup.build({ await tsup.build({
...baseConfig("nextjs"), ...baseConfig("nextjs"),
platform: "node" platform: "node",
}); });
await tsup.build({ await tsup.build({
...baseConfig("node"), ...baseConfig("node"),
platform: "node" platform: "node",
}); });
} }

View File

@@ -5,8 +5,8 @@ export const entryOutputMeta = (
outputs: { outputs: {
output: string; output: string;
meta: Metafile["outputs"][string]; meta: Metafile["outputs"][string];
}[] }[],
) => void | Promise<void> ) => void | Promise<void>,
): Plugin => ({ ): Plugin => ({
name: "report-entry-output-plugin", name: "report-entry-output-plugin",
setup(build) { setup(build) {
@@ -29,5 +29,5 @@ export const entryOutputMeta = (
await onComplete?.(outputs); await onComplete?.(outputs);
} }
}); });
} },
}); });

View File

@@ -3,7 +3,7 @@
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,
"bin": "./dist/cli/index.js", "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.", "description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
"homepage": "https://bknd.io", "homepage": "https://bknd.io",
"repository": { "repository": {
@@ -32,79 +32,83 @@
}, },
"license": "FSL-1.1-MIT", "license": "FSL-1.1-MIT",
"dependencies": { "dependencies": {
"@cfworker/json-schema": "^2.0.1", "@cfworker/json-schema": "^4.1.1",
"@codemirror/lang-html": "^6.4.9", "@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-json": "^6.0.1", "@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-liquid": "^6.2.1", "@codemirror/lang-liquid": "^6.2.2",
"@hello-pangea/dnd": "^17.0.0", "@hello-pangea/dnd": "^18.0.1",
"@libsql/client": "^0.14.0", "@libsql/client": "^0.14.0",
"@mantine/core": "^7.13.4", "@mantine/core": "^7.17.1",
"@sinclair/typebox": "^0.32.34", "@mantine/hooks": "^7.17.1",
"@tanstack/react-form": "0.19.2", "@sinclair/typebox": "^0.34.30",
"@uiw/react-codemirror": "^4.23.6", "@tanstack/react-form": "^1.0.5",
"@xyflow/react": "^12.3.2", "@uiw/react-codemirror": "^4.23.10",
"aws4fetch": "^1.0.18", "@xyflow/react": "^12.4.4",
"aws4fetch": "^1.0.20",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"fast-xml-parser": "^4.4.0", "fast-xml-parser": "^5.0.8",
"hono": "^4.6.12", "hono": "^4.7.4",
"json-schema-form-react": "^0.0.2", "json-schema-form-react": "^0.0.2",
"json-schema-library": "^10.0.0-rc7", "json-schema-library": "^10.0.0-rc7",
"json-schema-to-ts": "^3.1.1", "json-schema-to-ts": "^3.1.1",
"kysely": "^0.27.4", "kysely": "^0.27.6",
"liquidjs": "^10.15.0", "liquidjs": "^10.21.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"oauth4webapi": "^2.11.1", "oauth4webapi": "^2.11.1",
"object-path-immutable": "^4.1.2", "object-path-immutable": "^4.1.2",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"radix-ui": "^1.1.2", "radix-ui": "^1.1.3",
"swr": "^2.2.5" "swr": "^2.3.3"
}, },
"devDependencies": { "devDependencies": {
"@aws-sdk/client-s3": "^3.613.0", "@aws-sdk/client-s3": "^3.758.0",
"@bluwy/giget-core": "^0.1.2", "@bluwy/giget-core": "^0.1.2",
"@dagrejs/dagre": "^1.1.4", "@dagrejs/dagre": "^1.1.4",
"@hono/typebox-validator": "^0.2.6", "@hono/typebox-validator": "^0.3.2",
"@hono/vite-dev-server": "^0.17.0", "@hono/vite-dev-server": "^0.19.0",
"@hono/zod-validator": "^0.4.1", "@hookform/resolvers": "^4.1.3",
"@hookform/resolvers": "^3.9.1",
"@libsql/kysely-libsql": "^0.4.1", "@libsql/kysely-libsql": "^0.4.1",
"@rjsf/core": "^5.22.2", "@mantine/modals": "^7.17.1",
"@mantine/notifications": "^7.17.1",
"@rjsf/core": "5.22.2",
"@tabler/icons-react": "3.18.0", "@tabler/icons-react": "3.18.0",
"@types/node": "^22.10.0", "@tailwindcss/postcss": "^4.0.12",
"@types/react": "^18.3.12", "@tailwindcss/vite": "^4.0.12",
"@types/react-dom": "^18.3.1", "@types/node": "^22.13.10",
"@vitejs/plugin-react": "^4.3.3", "@types/react": "^19.0.10",
"autoprefixer": "^10.4.20", "@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.21",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"esbuild-postcss": "^0.0.4", "jotai": "^2.12.2",
"jotai": "^2.10.1",
"kysely-d1": "^0.3.0", "kysely-d1": "^0.3.0",
"open": "^10.1.0", "open": "^10.1.0",
"openapi-types": "^12.1.3", "openapi-types": "^12.1.3",
"postcss": "^8.4.47", "postcss": "^8.5.3",
"postcss-preset-mantine": "^1.17.0", "postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1", "postcss-simple-vars": "^7.0.1",
"react-hook-form": "^7.53.1", "react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.54.2",
"react-icons": "5.2.1", "react-icons": "5.2.1",
"react-json-view-lite": "^2.0.1", "react-json-view-lite": "^2.4.1",
"sql-formatter": "^15.4.9", "sql-formatter": "^15.4.11",
"tailwind-merge": "^2.5.4", "tailwind-merge": "^3.0.2",
"tailwindcss": "^3.4.14", "tailwindcss": "^4.0.12",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"tsc-alias": "^1.8.10", "tsc-alias": "^1.8.11",
"tsup": "^8.3.5", "tsup": "^8.4.0",
"vite": "^5.4.10", "vite": "^6.2.1",
"vite-plugin-static-copy": "^2.0.0", "vite-tsconfig-paths": "^5.1.4",
"vite-tsconfig-paths": "^5.0.1", "wouter": "^3.6.0"
"wouter": "^3.3.5"
}, },
"optionalDependencies": { "optionalDependencies": {
"@hono/node-server": "^1.13.7" "@hono/node-server": "^1.13.8"
}, },
"peerDependencies": { "peerDependencies": {
"react": ">=18", "react": "^19.x",
"react-dom": ">=18" "react-dom": "^19.x"
}, },
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -189,6 +193,11 @@
"import": "./dist/adapter/astro/index.js", "import": "./dist/adapter/astro/index.js",
"require": "./dist/adapter/astro/index.cjs" "require": "./dist/adapter/astro/index.cjs"
}, },
"./adapter/aws": {
"types": "./dist/types/adapter/aws/index.d.ts",
"import": "./dist/adapter/aws/index.js",
"require": "./dist/adapter/aws/index.cjs"
},
"./dist/main.css": "./dist/ui/main.css", "./dist/main.css": "./dist/ui/main.css",
"./dist/styles.css": "./dist/ui/styles.css", "./dist/styles.css": "./dist/ui/styles.css",
"./dist/manifest.json": "./dist/static/.vite/manifest.json" "./dist/manifest.json": "./dist/static/.vite/manifest.json"
@@ -221,4 +230,4 @@
"bun", "bun",
"node" "node"
] ]
} }

View File

@@ -1,9 +1,6 @@
export default { export default {
plugins: { plugins: {
"postcss-import": {}, "@tailwindcss/postcss": {},
"tailwindcss/nesting": {},
tailwindcss: {},
autoprefixer: {},
"postcss-preset-mantine": {}, "postcss-preset-mantine": {},
"postcss-simple-vars": { "postcss-simple-vars": {
variables: { variables: {
@@ -11,8 +8,8 @@ export default {
"mantine-breakpoint-sm": "48em", "mantine-breakpoint-sm": "48em",
"mantine-breakpoint-md": "62em", "mantine-breakpoint-md": "62em",
"mantine-breakpoint-lg": "75em", "mantine-breakpoint-lg": "75em",
"mantine-breakpoint-xl": "88em" "mantine-breakpoint-xl": "88em",
} },
} },
} },
}; };

View File

@@ -2,9 +2,9 @@ import type { SafeUser } from "auth";
import { AuthApi } from "auth/api/AuthApi"; import { AuthApi } from "auth/api/AuthApi";
import { DataApi } from "data/api/DataApi"; import { DataApi } from "data/api/DataApi";
import { decode } from "hono/jwt"; import { decode } from "hono/jwt";
import { omit } from "lodash-es";
import { MediaApi } from "media/api/MediaApi"; import { MediaApi } from "media/api/MediaApi";
import { SystemApi } from "modules/SystemApi"; import { SystemApi } from "modules/SystemApi";
import { omitKeys } from "core/utils";
export type TApiUser = SafeUser; export type TApiUser = SafeUser;
@@ -122,7 +122,7 @@ export class Api {
this.verified = false; this.verified = false;
if (token) { if (token) {
this.user = omit(decode(token).payload as any, ["iat", "iss", "exp"]) as any; this.user = omitKeys(decode(token).payload as any, ["iat", "iss", "exp"]) as any;
} else { } else {
this.user = undefined; this.user = undefined;
} }
@@ -153,7 +153,7 @@ export class Api {
return { return {
token: this.token, token: this.token,
user: this.user, user: this.user,
verified: this.verified verified: this.verified,
}; };
} }
@@ -198,7 +198,7 @@ export class Api {
token: this.token, token: this.token,
headers: this.options.headers, headers: this.options.headers,
token_transport: this.token_transport, token_transport: this.token_transport,
verbose: this.options.verbose verbose: this.options.verbose,
}); });
} }
@@ -211,9 +211,9 @@ export class Api {
this.auth = new AuthApi( this.auth = new AuthApi(
{ {
...baseParams, ...baseParams,
onTokenUpdate: (token) => this.updateToken(token, true) onTokenUpdate: (token) => this.updateToken(token, true),
}, },
fetcher fetcher,
); );
this.media = new MediaApi(baseParams, fetcher); this.media = new MediaApi(baseParams, fetcher);
} }

View File

@@ -1,4 +1,3 @@
import { Api, type ApiOptions } from "Api";
import type { CreateUserPayload } from "auth/AppAuth"; import type { CreateUserPayload } from "auth/AppAuth";
import { $console } from "core"; import { $console } from "core";
import { Event } from "core/events"; import { Event } from "core/events";
@@ -9,12 +8,15 @@ import {
type ModuleBuildContext, type ModuleBuildContext,
ModuleManager, ModuleManager,
type ModuleManagerOptions, type ModuleManagerOptions,
type Modules type Modules,
} from "modules/ModuleManager"; } from "modules/ModuleManager";
import * as SystemPermissions from "modules/permissions"; import * as SystemPermissions from "modules/permissions";
import { AdminController, type AdminControllerOptions } from "modules/server/AdminController"; import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
import { SystemController } from "modules/server/SystemController"; import { SystemController } from "modules/server/SystemController";
// biome-ignore format: must be there
import { Api, type ApiOptions } from "Api";
export type AppPlugin = (app: App) => Promise<void> | void; export type AppPlugin = (app: App) => Promise<void> | void;
abstract class AppEvent<A = {}> extends Event<{ app: App } & A> {} abstract class AppEvent<A = {}> extends Event<{ app: App } & A> {}
@@ -31,7 +33,7 @@ export const AppEvents = { AppConfigUpdatedEvent, AppBuiltEvent, AppFirstBoot }
export type AppOptions = { export type AppOptions = {
plugins?: AppPlugin[]; plugins?: AppPlugin[];
seed?: (ctx: ModuleBuildContext) => Promise<void>; seed?: (ctx: ModuleBuildContext & { app: App }) => Promise<void>;
manager?: Omit<ModuleManagerOptions, "initial" | "onUpdated" | "seed">; manager?: Omit<ModuleManagerOptions, "initial" | "onUpdated" | "seed">;
}; };
export type CreateAppConfig = { export type CreateAppConfig = {
@@ -48,6 +50,7 @@ export type CreateAppConfig = {
}; };
export type AppConfig = InitialModuleConfigs; export type AppConfig = InitialModuleConfigs;
export type LocalApiOptions = Request | ApiOptions;
export class App { export class App {
modules: ModuleManager; modules: ModuleManager;
@@ -55,17 +58,18 @@ export class App {
adminController?: AdminController; adminController?: AdminController;
private trigger_first_boot = false; private trigger_first_boot = false;
private plugins: AppPlugin[]; private plugins: AppPlugin[];
private _id: string = crypto.randomUUID();
private _building: boolean = false;
constructor( constructor(
private connection: Connection, private connection: Connection,
_initialConfig?: InitialModuleConfigs, _initialConfig?: InitialModuleConfigs,
private options?: AppOptions private options?: AppOptions,
) { ) {
this.plugins = options?.plugins ?? []; this.plugins = options?.plugins ?? [];
this.modules = new ModuleManager(connection, { this.modules = new ModuleManager(connection, {
...(options?.manager ?? {}), ...(options?.manager ?? {}),
initial: _initialConfig, initial: _initialConfig,
seed: options?.seed,
onUpdated: async (key, config) => { onUpdated: async (key, config) => {
// if the EventManager was disabled, we assume we shouldn't // if the EventManager was disabled, we assume we shouldn't
// respond to events, such as "onUpdated". // respond to events, such as "onUpdated".
@@ -88,8 +92,13 @@ export class App {
server.use(async (c, next) => { server.use(async (c, next) => {
c.set("app", this); c.set("app", this);
await next(); await next();
try {
// gracefully add the app id
c.res.headers.set("X-bknd-id", this._id);
} catch (e) {}
}); });
} },
}); });
this.modules.ctx().emgr.registerEvents(AppEvents); this.modules.ctx().emgr.registerEvents(AppEvents);
} }
@@ -98,9 +107,18 @@ export class App {
return this.modules.ctx().emgr; 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; 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(); const { guard, server } = this.modules.ctx();
@@ -113,13 +131,20 @@ export class App {
await Promise.all(this.plugins.map((plugin) => plugin(this))); await Promise.all(this.plugins.map((plugin) => plugin(this)));
} }
$console.log("App built");
await this.emgr.emit(new AppBuiltEvent({ app: this })); await this.emgr.emit(new AppBuiltEvent({ app: this }));
// first boot is set from ModuleManager when there wasn't a config table // first boot is set from ModuleManager when there wasn't a config table
if (this.trigger_first_boot) { if (this.trigger_first_boot) {
this.trigger_first_boot = false; this.trigger_first_boot = false;
await this.emgr.emit(new AppFirstBoot({ app: this })); await this.emgr.emit(new AppFirstBoot({ app: this }));
await this.options?.seed?.({
...this.modules.ctx(),
app: this,
});
} }
this._building = false;
} }
mutateConfig<Module extends keyof Modules>(module: Module) { mutateConfig<Module extends keyof Modules>(module: Module) {
@@ -144,8 +169,8 @@ export class App {
{ {
get: (_, module: keyof Modules) => { get: (_, module: keyof Modules) => {
return this.modules.get(module); return this.modules.get(module);
} },
} },
) as Modules; ) as Modules;
} }
@@ -180,13 +205,13 @@ export class App {
return this.module.auth.createUser(p); return this.module.auth.createUser(p);
} }
getApi(options: Request | ApiOptions = {}) { getApi(options?: LocalApiOptions) {
const fetcher = this.server.request as typeof fetch; 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({ 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") { } else if (typeof config.connection === "object") {
if ("type" in config.connection) { if ("type" in config.connection) {
$console.warn( $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); connection = new LibsqlConnection(config.connection.config);
} else { } else {

View File

@@ -17,7 +17,7 @@ export type Options = {
export async function getApi(Astro: TAstro, options: Options = { mode: "static" }) { export async function getApi(Astro: TAstro, options: Options = { mode: "static" }) {
const api = new Api({ const api = new Api({
host: new URL(Astro.request.url).origin, host: new URL(Astro.request.url).origin,
headers: options.mode === "dynamic" ? Astro.request.headers : undefined headers: options.mode === "dynamic" ? Astro.request.headers : undefined,
}); });
await api.verifyAuth(); await api.verifyAuth();
return api; return api;

View 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);
};
}

View File

@@ -0,0 +1 @@
export * from "./aws-lambda.adapter";

View File

@@ -19,7 +19,7 @@ export async function createApp({ distPath, ...config }: RuntimeBkndConfig = {})
registerLocalMediaAdapter(); registerLocalMediaAdapter();
app = await createRuntimeApp({ app = await createRuntimeApp({
...config, ...config,
serveStatic: serveStatic({ root }) serveStatic: serveStatic({ root }),
}); });
} }
@@ -34,6 +34,7 @@ export function serve({
port = config.server.default_port, port = config.server.default_port,
onBuilt, onBuilt,
buildConfig, buildConfig,
adminOptions,
...serveOptions ...serveOptions
}: BunBkndConfig = {}) { }: BunBkndConfig = {}) {
Bun.serve({ Bun.serve({
@@ -46,10 +47,11 @@ export function serve({
options, options,
onBuilt, onBuilt,
buildConfig, buildConfig,
distPath adminOptions,
distPath,
}); });
return app.fetch(request); return app.fetch(request);
} },
}); });
console.log(`Server is running on http://localhost:${port}`); console.log(`Server is running on http://localhost:${port}`);

View File

@@ -12,7 +12,7 @@ export type D1ConnectionConfig = {
class CustomD1Dialect extends D1Dialect { class CustomD1Dialect extends D1Dialect {
override createIntrospector(db: Kysely<any>): DatabaseIntrospector { override createIntrospector(db: Kysely<any>): DatabaseIntrospector {
return new SqliteIntrospector(db, { return new SqliteIntrospector(db, {
excludeTables: ["_cf_KV"] excludeTables: ["_cf_KV"],
}); });
} }
} }
@@ -23,7 +23,7 @@ export class D1Connection extends SqliteConnection {
const kysely = new Kysely({ const kysely = new Kysely({
dialect: new CustomD1Dialect({ database: config.binding }), dialect: new CustomD1Dialect({ database: config.binding }),
plugins plugins,
}); });
super(kysely, {}, plugins); super(kysely, {}, plugins);
} }
@@ -37,7 +37,7 @@ export class D1Connection extends SqliteConnection {
} }
protected override async batch<Queries extends QB[]>( protected override async batch<Queries extends QB[]>(
queries: [...Queries] queries: [...Queries],
): Promise<{ ): Promise<{
[K in keyof Queries]: Awaited<ReturnType<Queries[K]["execute"]>>; [K in keyof Queries]: Awaited<ReturnType<Queries[K]["execute"]>>;
}> { }> {
@@ -47,7 +47,7 @@ export class D1Connection extends SqliteConnection {
queries.map((q) => { queries.map((q) => {
const { sql, parameters } = q.compile(); const { sql, parameters } = q.compile();
return db.prepare(sql).bind(...parameters); return db.prepare(sql).bind(...parameters);
}) }),
); );
// let it run through plugins // let it run through plugins

View File

@@ -8,9 +8,9 @@ import { getBindings } from "./bindings";
export function makeSchema(bindings: string[] = []) { export function makeSchema(bindings: string[] = []) {
return Type.Object( 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() { override toJSON() {
return { return {
...super.toJSON(), ...super.toJSON(),
config: this.config config: this.config,
}; };
} }
} },
); );
} }
@@ -67,13 +67,13 @@ export class StorageR2Adapter implements StorageAdapter {
} }
} }
async listObjects( async listObjects(
prefix?: string prefix?: string,
): Promise<{ key: string; last_modified: Date; size: number }[]> { ): Promise<{ key: string; last_modified: Date; size: number }[]> {
const list = await this.bucket.list({ limit: 50 }); const list = await this.bucket.list({ limit: 50 });
return list.objects.map((item) => ({ return list.objects.map((item) => ({
key: item.key, key: item.key,
size: item.size, size: item.size,
last_modified: item.uploaded last_modified: item.uploaded,
})); }));
} }
@@ -89,7 +89,7 @@ export class StorageR2Adapter implements StorageAdapter {
let object: R2ObjectBody | null; let object: R2ObjectBody | null;
const responseHeaders = new Headers({ const responseHeaders = new Headers({
"Accept-Ranges": "bytes", "Accept-Ranges": "bytes",
"Content-Type": guess(key) "Content-Type": guess(key),
}); });
//console.log("getObject:headers", headersToObject(headers)); //console.log("getObject:headers", headersToObject(headers));
@@ -98,7 +98,7 @@ export class StorageR2Adapter implements StorageAdapter {
? {} // miniflare doesn't support range requests ? {} // miniflare doesn't support range requests
: { : {
range: headers, range: headers,
onlyIf: headers onlyIf: headers,
}; };
object = (await this.bucket.get(key, options)) as R2ObjectBody; object = (await this.bucket.get(key, options)) as R2ObjectBody;
@@ -130,7 +130,7 @@ export class StorageR2Adapter implements StorageAdapter {
return new Response(object.body, { return new Response(object.body, {
status: object.range ? 206 : 200, status: object.range ? 206 : 200,
headers: responseHeaders headers: responseHeaders,
}); });
} }
@@ -139,7 +139,7 @@ export class StorageR2Adapter implements StorageAdapter {
if (!metadata || Object.keys(metadata).length === 0) { if (!metadata || Object.keys(metadata).length === 0) {
// guessing is especially required for dev environment (miniflare) // guessing is especially required for dev environment (miniflare)
metadata = { metadata = {
contentType: guess(object.key) contentType: guess(object.key),
}; };
} }
@@ -157,7 +157,7 @@ export class StorageR2Adapter implements StorageAdapter {
return { return {
type: String(head.httpMetadata?.contentType ?? guess(key)), 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) { toJSON(secrets?: boolean) {
return { return {
type: this.getName(), type: this.getName(),
config: {} config: {},
}; };
} }
} }

View File

@@ -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) { if (env[key] && (env[key] as any).constructor.name === type) {
bindings.push({ bindings.push({
key, key,
value: env[key] as BindingTypeMap[T] value: env[key] as BindingTypeMap[T],
}); });
} }
} catch (e) {} } catch (e) {}

View File

@@ -84,7 +84,7 @@ export function serve<Env = any>(config: CloudflareBkndConfig<Env> = {}) {
hono.all("*", async (c, next) => { hono.all("*", async (c, next) => {
const res = await serveStatic({ const res = await serveStatic({
path: `./${pathname}`, path: `./${pathname}`,
manifest: config.manifest! manifest: config.manifest!,
})(c as any, next); })(c as any, next);
if (res instanceof Response) { if (res instanceof Response) {
const ttl = 60 * 60 * 24 * 365; const ttl = 60 * 60 * 24 * 365;
@@ -114,6 +114,6 @@ export function serve<Env = any>(config: CloudflareBkndConfig<Env> = {}) {
default: default:
throw new Error(`Unknown mode ${mode}`); throw new Error(`Unknown mode ${mode}`);
} }
} },
}; };
} }

View File

@@ -10,7 +10,7 @@ export {
getBindings, getBindings,
type BindingTypeMap, type BindingTypeMap,
type GetBindingType, type GetBindingType,
type BindingMap type BindingMap,
} from "./bindings"; } from "./bindings";
export function d1(config: D1ConnectionConfig) { export function d1(config: D1ConnectionConfig) {

View File

@@ -31,13 +31,13 @@ export async function getCached(config: CloudflareBkndConfig, { env, ctx, ...arg
async ({ params: { app } }) => { async ({ params: { app } }) => {
saveConfig(app.toJSON(true)); saveConfig(app.toJSON(true));
}, },
"sync" "sync",
); );
await config.beforeBuild?.(app); await config.beforeBuild?.(app);
}, },
adminOptions: { html: config.html } adminOptions: { html: config.html },
}, },
{ env, ctx, ...args } { env, ctx, ...args },
); );
if (!cachedConfig) { if (!cachedConfig) {

View File

@@ -23,7 +23,7 @@ export async function getDurable(config: CloudflareBkndConfig, ctx: Context) {
config: create_config, config: create_config,
html: config.html, html: config.html,
keepAliveSeconds: config.keepAliveSeconds, keepAliveSeconds: config.keepAliveSeconds,
setAdminHtml: config.setAdminHtml setAdminHtml: config.setAdminHtml,
}); });
const headers = new Headers(res.headers); const headers = new Headers(res.headers);
@@ -32,7 +32,7 @@ export async function getDurable(config: CloudflareBkndConfig, ctx: Context) {
return new Response(res.body, { return new Response(res.body, {
status: res.status, status: res.status,
statusText: res.statusText, statusText: res.statusText,
headers headers,
}); });
} }
@@ -48,7 +48,7 @@ export class DurableBkndApp extends DurableObject {
html?: string; html?: string;
keepAliveSeconds?: number; keepAliveSeconds?: number;
setAdminHtml?: boolean; setAdminHtml?: boolean;
} },
) { ) {
let buildtime = 0; let buildtime = 0;
if (!this.app) { if (!this.app) {
@@ -73,7 +73,7 @@ export class DurableBkndApp extends DurableObject {
return c.json({ return c.json({
id: this.id, id: this.id,
keepAliveSeconds: options?.keepAliveSeconds ?? 0, keepAliveSeconds: options?.keepAliveSeconds ?? 0,
colo: context.colo colo: context.colo,
}); });
}); });
@@ -82,7 +82,7 @@ export class DurableBkndApp extends DurableObject {
adminOptions: { html: options.html }, adminOptions: { html: options.html },
beforeBuild: async (app) => { beforeBuild: async (app) => {
await this.beforeBuild(app); await this.beforeBuild(app);
} },
}); });
buildtime = performance.now() - start; buildtime = performance.now() - start;
@@ -101,7 +101,7 @@ export class DurableBkndApp extends DurableObject {
return new Response(res.body, { return new Response(res.body, {
status: res.status, status: res.status,
statusText: res.statusText, statusText: res.statusText,
headers headers,
}); });
} }

View File

@@ -6,9 +6,9 @@ export async function makeApp(config: CloudflareBkndConfig, ctx: Context) {
return await createRuntimeApp( return await createRuntimeApp(
{ {
...makeCfConfig(config, ctx), ...makeCfConfig(config, ctx),
adminOptions: config.html ? { html: config.html } : undefined adminOptions: config.html ? { html: config.html } : undefined,
}, },
ctx ctx,
); );
} }

View File

@@ -14,6 +14,8 @@ export type FrameworkBkndConfig<Args = any> = BkndConfig<Args>;
export type RuntimeBkndConfig<Args = any> = BkndConfig<Args> & { export type RuntimeBkndConfig<Args = any> = BkndConfig<Args> & {
distPath?: string; distPath?: string;
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
adminOptions?: AdminControllerOptions | false;
}; };
export function makeConfig<Args = any>(config: BkndConfig<Args>, args?: Args): CreateAppConfig { 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>( export async function createFrameworkApp<Args = any>(
config: FrameworkBkndConfig, config: FrameworkBkndConfig,
args?: Args args?: Args,
): Promise<App> { ): Promise<App> {
const app = App.create(makeConfig(config, args)); const app = App.create(makeConfig(config, args));
@@ -44,7 +46,7 @@ export async function createFrameworkApp<Args = any>(
async () => { async () => {
await config.onBuilt?.(app); await config.onBuilt?.(app);
}, },
"sync" "sync",
); );
} }
@@ -55,15 +57,8 @@ export async function createFrameworkApp<Args = any>(
} }
export async function createRuntimeApp<Env = any>( export async function createRuntimeApp<Env = any>(
{ { serveStatic, adminOptions, ...config }: RuntimeBkndConfig,
serveStatic, env?: Env,
adminOptions,
...config
}: RuntimeBkndConfig & {
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
adminOptions?: AdminControllerOptions | false;
},
env?: Env
): Promise<App> { ): Promise<App> {
const app = App.create(makeConfig(config, env)); const app = App.create(makeConfig(config, env));
@@ -82,7 +77,7 @@ export async function createRuntimeApp<Env = any>(
app.registerAdminController(adminOptions); app.registerAdminController(adminOptions);
} }
}, },
"sync" "sync",
); );
await config.beforeBuild?.(app); await config.beforeBuild?.(app);

View File

@@ -1,64 +1,60 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import { nodeRequestToRequest } from "adapter/utils";
import type { App } from "bknd"; import type { App } from "bknd";
import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter"; import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter";
import { Api } from "bknd/client"; import { getRuntimeKey, isNode } from "core/utils";
export type NextjsBkndConfig = FrameworkBkndConfig & { export type NextjsBkndConfig = FrameworkBkndConfig & {
cleanSearch?: string[]; cleanRequest?: { searchParams?: string[] };
}; };
type GetServerSidePropsContext = { let app: App;
req: IncomingMessage; let building: boolean = false;
res: ServerResponse;
params?: Params;
query: any;
preview?: boolean;
previewData?: any;
draftMode?: boolean;
resolvedUrl: string;
locale?: string;
locales?: string[];
defaultLocale?: string;
};
export function createApi({ req }: GetServerSidePropsContext) { export async function getApp(config: NextjsBkndConfig) {
const request = nodeRequestToRequest(req); if (building) {
return new Api({ while (building) {
host: new URL(request.url).origin, await new Promise((resolve) => setTimeout(resolve, 5));
headers: request.headers }
}); if (app) return app;
}
building = true;
if (!app) {
app = await createFrameworkApp(config);
await app.build();
}
building = false;
return app;
} }
export function withApi<T>(handler: (ctx: GetServerSidePropsContext & { api: Api }) => T) { function getCleanRequest(req: Request, cleanRequest: NextjsBkndConfig["cleanRequest"]) {
return async (ctx: GetServerSidePropsContext & { api: Api }) => { if (!cleanRequest) return req;
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); const url = new URL(req.url);
cleanSearch?.forEach((k) => url.searchParams.delete(k)); cleanRequest?.searchParams?.forEach((k) => url.searchParams.delete(k));
if (isNode()) {
return new Request(url.toString(), {
method: req.method,
headers: req.headers,
body: req.body,
// @ts-ignore
duplex: "half",
});
}
return new Request(url.toString(), { return new Request(url.toString(), {
method: req.method, method: req.method,
headers: req.headers, headers: req.headers,
body: req.body body: req.body,
}); });
} }
let app: App; export function serve({ cleanRequest, ...config }: NextjsBkndConfig = {}) {
export function serve({ cleanSearch, ...config }: NextjsBkndConfig = {}) {
return async (req: Request) => { return async (req: Request) => {
if (!app) { if (!app) {
app = await createFrameworkApp(config); app = await getApp(config);
} }
const request = getCleanRequest(req, { cleanSearch }); const request = getCleanRequest(req, cleanRequest);
return app.fetch(request, process.env); return app.fetch(request);
}; };
} }

View File

@@ -1,7 +1,7 @@
import { registries } from "bknd"; import { registries } from "bknd";
import { import {
type LocalAdapterConfig, type LocalAdapterConfig,
StorageLocalAdapter StorageLocalAdapter,
} from "../../media/storage/adapters/StorageLocalAdapter"; } from "../../media/storage/adapters/StorageLocalAdapter";
export * from "./node.adapter"; export * from "./node.adapter";

View File

@@ -24,7 +24,7 @@ export function serve({
}: NodeBkndConfig = {}) { }: NodeBkndConfig = {}) {
const root = path.relative( const root = path.relative(
process.cwd(), process.cwd(),
path.resolve(distPath ?? relativeDistPath ?? "./node_modules/bknd/dist", "static") path.resolve(distPath ?? relativeDistPath ?? "./node_modules/bknd/dist", "static"),
); );
if (relativeDistPath) { if (relativeDistPath) {
console.warn("relativeDistPath is deprecated, please use distPath instead"); console.warn("relativeDistPath is deprecated, please use distPath instead");
@@ -41,16 +41,16 @@ export function serve({
registerLocalMediaAdapter(); registerLocalMediaAdapter();
app = await createRuntimeApp({ app = await createRuntimeApp({
...config, ...config,
serveStatic: serveStatic({ root }) serveStatic: serveStatic({ root }),
}); });
} }
return app.fetch(req); return app.fetch(req);
} },
}, },
(connInfo) => { (connInfo) => {
console.log(`Server is running on http://localhost:${connInfo.port}`); console.log(`Server is running on http://localhost:${connInfo.port}`);
listener?.(connInfo); listener?.(connInfo);
} },
); );
} }

View File

@@ -10,7 +10,10 @@ type RemixContext = {
let app: App; let app: App;
let building: boolean = false; 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) { if (building) {
while (building) { while (building) {
await new Promise((resolve) => setTimeout(resolve, 5)); 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>( export function serve<Args extends RemixContext = RemixContext>(
config: RemixBkndConfig<Args> = {} config: RemixBkndConfig<Args> = {},
) { ) {
return async (args: Args) => { return async (args: Args) => {
app = await createFrameworkApp(config, args); app = await getApp(config, args);
return app.fetch(args.request); return app.fetch(args.request);
}; };
} }

View File

@@ -20,6 +20,6 @@ export function nodeRequestToRequest(req: IncomingMessage): Request {
const method = req.method || "GET"; const method = req.method || "GET";
return new Request(url, { return new Request(url, {
method, method,
headers headers,
}); });
} }

View File

@@ -8,7 +8,7 @@ export const devServerConfig = {
/^\/@.+$/, /^\/@.+$/,
/\/components.*?\.json.*/, // @todo: improve /\/components.*?\.json.*/, // @todo: improve
/^\/(public|assets|static)\/.+/, /^\/(public|assets|static)\/.+/,
/^\/node_modules\/.*/ /^\/node_modules\/.*/,
] as any, ] as any,
injectClientScript: false injectClientScript: false,
} as const; } as const;

View File

@@ -24,7 +24,7 @@ window.__vite_plugin_react_preamble_installed__ = true
</script> </script>
<script type="module" src="/@vite/client"></script> <script type="module" src="/@vite/client"></script>
${addBkndContext ? "<!-- BKND_CONTEXT -->" : ""} ${addBkndContext ? "<!-- BKND_CONTEXT -->" : ""}
</head>` </head>`,
); );
} }
@@ -39,12 +39,12 @@ async function createApp(config: ViteBkndConfig = {}, env?: any) {
: { : {
html: config.html, html: config.html,
forceDev: config.forceDev ?? { forceDev: config.forceDev ?? {
mainPath: "/src/main.tsx" mainPath: "/src/main.tsx",
} },
}, },
serveStatic: ["/assets/*", serveStatic({ root: config.distPath ?? "./" })] serveStatic: ["/assets/*", serveStatic({ root: config.distPath ?? "./" })],
}, },
env env,
); );
} }
@@ -53,7 +53,7 @@ export function serveFresh(config: Omit<ViteBkndConfig, "mode"> = {}) {
async fetch(request: Request, env: any, ctx: ExecutionContext) { async fetch(request: Request, env: any, ctx: ExecutionContext) {
const app = await createApp(config, env); const app = await createApp(config, env);
return app.fetch(request, env, ctx); return app.fetch(request, env, ctx);
} },
}; };
} }
@@ -66,7 +66,7 @@ export function serveCached(config: Omit<ViteBkndConfig, "mode"> = {}) {
} }
return app.fetch(request, env, ctx); return app.fetch(request, env, ctx);
} },
}; };
} }
@@ -77,6 +77,6 @@ export function serve({ mode, ...config }: ViteBkndConfig = {}) {
export function devServer(options: DevServerOptions) { export function devServer(options: DevServerOptions) {
return honoViteDevServer({ return honoViteDevServer({
...devServerConfig, ...devServerConfig,
...options ...options,
}); });
} }

View File

@@ -4,10 +4,10 @@ import {
Authenticator, Authenticator,
type ProfileExchange, type ProfileExchange,
Role, Role,
type Strategy type Strategy,
} from "auth"; } from "auth";
import type { PasswordStrategy } from "auth/authenticate/strategies"; 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 Static, secureRandomString, transformObject } from "core/utils";
import type { Entity, EntityManager } from "data"; import type { Entity, EntityManager } from "data";
import { type FieldSchema, em, entity, enumm, text } from "data/prototype"; 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; return to;
} }
@@ -56,7 +62,6 @@ export class AppAuth extends Module<typeof authConfigSchema> {
// register roles // register roles
const roles = transformObject(this.config.roles ?? {}, (role, name) => { const roles = transformObject(this.config.roles ?? {}, (role, name) => {
//console.log("role", role, name);
return Role.create({ name, ...role }); return Role.create({ name, ...role });
}); });
this.ctx.guard.setRoles(Object.values(roles)); this.ctx.guard.setRoles(Object.values(roles));
@@ -69,15 +74,15 @@ export class AppAuth extends Module<typeof authConfigSchema> {
} catch (e) { } catch (e) {
throw new Error( throw new Error(
`Could not build strategy ${String( `Could not build strategy ${String(
name name,
)} with config ${JSON.stringify(strategy.config)}` )} with config ${JSON.stringify(strategy.config)}`,
); );
} }
}); });
this._authenticator = new Authenticator(strategies, this.resolveUser.bind(this), { this._authenticator = new Authenticator(strategies, this.resolveUser.bind(this), {
jwt: this.config.jwt, jwt: this.config.jwt,
cookie: this.config.cookie cookie: this.config.cookie,
}); });
this.registerEntities(); this.registerEntities();
@@ -88,6 +93,14 @@ export class AppAuth extends Module<typeof authConfigSchema> {
this.ctx.guard.registerPermissions(Object.values(AuthPermissions)); 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 { get controller(): AuthController {
if (!this.isBuilt()) { if (!this.isBuilt()) {
throw new Error("Can't access controller, AppAuth not built yet"); throw new Error("Can't access controller, AppAuth not built yet");
@@ -113,14 +126,8 @@ export class AppAuth extends Module<typeof authConfigSchema> {
action: AuthAction, action: AuthAction,
strategy: Strategy, strategy: Strategy,
identifier: string, identifier: string,
profile: ProfileExchange profile: ProfileExchange,
): Promise<any> { ): Promise<any> {
/*console.log("***** AppAuth:resolveUser", {
action,
strategy: strategy.getName(),
identifier,
profile
});*/
if (!this.config.allow_register && action === "register") { if (!this.config.allow_register && action === "register") {
throw new Exception("Registration is not allowed", 403); throw new Exception("Registration is not allowed", 403);
} }
@@ -129,7 +136,7 @@ export class AppAuth extends Module<typeof authConfigSchema> {
.getFillableFields("create") .getFillableFields("create")
.map((f) => f.name); .map((f) => f.name);
const filteredProfile = Object.fromEntries( const filteredProfile = Object.fromEntries(
Object.entries(profile).filter(([key]) => fields.includes(key)) Object.entries(profile).filter(([key]) => fields.includes(key)),
); );
switch (action) { switch (action) {
@@ -141,21 +148,10 @@ export class AppAuth extends Module<typeof authConfigSchema> {
} }
private filterUserData(user: any) { 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); return pick(user, this.config.jwt.fields);
} }
private async login(strategy: Strategy, identifier: string, profile: ProfileExchange) { private async login(strategy: Strategy, identifier: string, profile: ProfileExchange) {
/*console.log("--- trying to login", {
strategy: strategy.getName(),
identifier,
profile
});*/
if (!("email" in profile)) { if (!("email" in profile)) {
throw new Exception("Profile must have email"); throw new Exception("Profile must have email");
} }
@@ -172,18 +168,14 @@ export class AppAuth extends Module<typeof authConfigSchema> {
if (!result.data) { if (!result.data) {
throw new Exception("User not found", 404); throw new Exception("User not found", 404);
} }
//console.log("---login data", result.data, result);
// compare strategy and identifier // compare strategy and identifier
//console.log("strategy comparison", result.data.strategy, strategy.getName());
if (result.data.strategy !== strategy.getName()) { if (result.data.strategy !== strategy.getName()) {
//console.log("!!! User registered with different strategy"); //console.log("!!! User registered with different strategy");
throw new Exception("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) { if (result.data.strategy_value !== identifier) {
//console.log("!!! Invalid credentials");
throw new Exception("Invalid credentials"); throw new Exception("Invalid credentials");
} }
@@ -207,7 +199,7 @@ export class AppAuth extends Module<typeof authConfigSchema> {
const payload: any = { const payload: any = {
...profile, ...profile,
strategy: strategy.getName(), strategy: strategy.getName(),
strategy_value: identifier strategy_value: identifier,
}; };
const mutator = this.em.mutator(users); const mutator = this.em.mutator(users);
@@ -257,13 +249,13 @@ export class AppAuth extends Module<typeof authConfigSchema> {
email: text().required(), email: text().required(),
strategy: text({ strategy: text({
fillable: ["create"], fillable: ["create"],
hidden: ["update", "form"] hidden: ["update", "form"],
}).required(), }).required(),
strategy_value: text({ strategy_value: text({
fillable: ["create"], fillable: ["create"],
hidden: ["read", "table", "update", "form"] hidden: ["read", "table", "update", "form"],
}).required(), }).required(),
role: text() role: text(),
}; };
registerEntities() { registerEntities() {
@@ -271,12 +263,12 @@ export class AppAuth extends Module<typeof authConfigSchema> {
this.ensureSchema( this.ensureSchema(
em( em(
{ {
[users.name as "users"]: users [users.name as "users"]: users,
}, },
({ index }, { users }) => { ({ index }, { users }) => {
index(users).on(["email"], true).on(["strategy"]).on(["strategy_value"]); index(users).on(["email"], true).on(["strategy"]).on(["strategy_value"]);
} },
) ),
); );
try { try {
@@ -285,6 +277,7 @@ export class AppAuth extends Module<typeof authConfigSchema> {
} catch (e) {} } catch (e) {}
try { try {
// also keep disabled strategies as a choice
const strategies = Object.keys(this.config.strategies ?? {}); const strategies = Object.keys(this.config.strategies ?? {});
this.replaceEntityField(users, "strategy", enumm({ enum: strategies })); this.replaceEntityField(users, "strategy", enumm({ enum: strategies }));
} catch (e) {} } catch (e) {}
@@ -304,7 +297,7 @@ export class AppAuth extends Module<typeof authConfigSchema> {
...(additional as any), ...(additional as any),
email, email,
strategy, strategy,
strategy_value strategy_value,
}); });
mutator.__unstable_toggleSystemEntityCreation(true); mutator.__unstable_toggleSystemEntityCreation(true);
return created; return created;
@@ -315,9 +308,16 @@ export class AppAuth extends Module<typeof authConfigSchema> {
return this.configDefault; return this.configDefault;
} }
const strategies = this.authenticator.getStrategies();
return { return {
...this.config, ...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