diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..9f032d0
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -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
\ No newline at end of file
diff --git a/README.md b/README.md
index e2add07..644f0d0 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@

-
+
⭐ Live Demo
@@ -18,13 +18,15 @@ bknd simplifies app development by providing a fully functional backend for data
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
## Size
-
-
-
-
+
+
+
+
The size on npm is misleading, as the `bknd` package includes the backend, the ui components as well as the whole backend bundled into the cli including static assets.
+Depending on what you use, the size can be higher as additional dependencies are getting pulled in. The minimal size of a full `bknd` app as an API is around 212 kB gzipped (e.g. deployed as Cloudflare Worker).
+
## Motivation
Creating digital products always requires developing both the backend (the logic) and the frontend (the appearance). Building a backend from scratch demands deep knowledge in areas such as authentication and database management. Using a backend framework can speed up initial development, but it still requires ongoing effort to work within its constraints (e.g., *"how to do X with Y?"*), which can quickly slow you down. Choosing a backend system is a tough decision, as you might not be aware of its limitations until you encounter them.
@@ -78,7 +80,7 @@ export default function AdminPage() {
### Using the REST API or TypeScript SDK (`bknd/client`)
If you're not using a JavaScript environment, you can still access any endpoint using the REST API:
```bash
-curl -XGET /api/data/
+curl -XGET /api/data/entity/
{
"data": [
{ "id": 1, ... },
diff --git a/app/__test__/api/Api.spec.ts b/app/__test__/api/Api.spec.ts
index 85f144d..dee1e14 100644
--- a/app/__test__/api/Api.spec.ts
+++ b/app/__test__/api/Api.spec.ts
@@ -19,8 +19,8 @@ describe("Api", async () => {
const token = await sign({ foo: "bar" }, "1234");
const request = new Request("http://example.com/test", {
headers: {
- Authorization: `Bearer ${token}`
- }
+ Authorization: `Bearer ${token}`,
+ },
});
const api = new Api({ request });
expect(api.isAuthVerified()).toBe(false);
@@ -35,8 +35,8 @@ describe("Api", async () => {
const token = await sign({ foo: "bar" }, "1234");
const request = new Request("http://example.com/test", {
headers: {
- Cookie: `auth=${token}`
- }
+ Cookie: `auth=${token}`,
+ },
});
const api = new Api({ request });
expect(api.isAuthVerified()).toBe(false);
diff --git a/app/__test__/api/DataApi.spec.ts b/app/__test__/api/DataApi.spec.ts
index c3c997a..c6bb3df 100644
--- a/app/__test__/api/DataApi.spec.ts
+++ b/app/__test__/api/DataApi.spec.ts
@@ -26,7 +26,7 @@ describe("DataApi", () => {
it("returns result", async () => {
const schema = proto.em({
- posts: proto.entity("posts", { title: proto.text() })
+ posts: proto.entity("posts", { title: proto.text() }),
});
const em = schemaToEm(schema);
await em.schema().sync({ force: true });
@@ -40,19 +40,17 @@ describe("DataApi", () => {
{
const res = (await app.request("/entity/posts")) as Response;
- const { data } = await res.json();
+ const { data } = (await res.json()) as any;
expect(data.length).toEqual(3);
}
// @ts-ignore tests
- const api = new DataApi({ basepath: "/", queryLengthLimit: 50 });
- // @ts-ignore protected
- api.fetcher = app.request as typeof fetch;
+ const api = new DataApi({ basepath: "/", queryLengthLimit: 50 }, app.request as typeof fetch);
{
const req = api.readMany("posts", { select: ["title"] });
expect(req.request.method).toBe("GET");
const res = await req;
- expect(res.data).toEqual(payload);
+ expect(res.data).toEqual(payload as any);
}
{
@@ -60,11 +58,155 @@ describe("DataApi", () => {
select: ["title"],
limit: 100000,
offset: 0,
- sort: "id"
+ sort: "id",
});
expect(req.request.method).toBe("POST");
const res = await req;
- expect(res.data).toEqual(payload);
+ expect(res.data).toEqual(payload as any);
+ }
+ });
+
+ it("updates many", async () => {
+ const schema = proto.em({
+ posts: proto.entity("posts", { title: proto.text(), count: proto.number() }),
+ });
+ const em = schemaToEm(schema);
+ await em.schema().sync({ force: true });
+
+ const payload = [
+ { title: "foo", count: 0 },
+ { title: "bar", count: 0 },
+ { title: "baz", count: 0 },
+ { title: "bla", count: 2 },
+ ];
+ await em.mutator("posts").insertMany(payload);
+
+ const ctx: any = { em, guard: new Guard() };
+ const controller = new DataController(ctx, dataConfig);
+ const app = controller.getController();
+
+ // @ts-ignore tests
+ const api = new DataApi({ basepath: "/" }, app.request as typeof fetch);
+ {
+ const req = api.readMany("posts", {
+ select: ["title", "count"],
+ });
+ const res = await req;
+ expect(res.data).toEqual(payload as any);
+ }
+
+ {
+ // update with empty where
+ expect(() => api.updateMany("posts", {}, { count: 1 })).toThrow();
+ expect(() => api.updateMany("posts", undefined, { count: 1 })).toThrow();
+ }
+
+ {
+ // update
+ const req = await api.updateMany("posts", { count: 0 }, { count: 1 });
+ expect(req.res.status).toBe(200);
+ }
+
+ {
+ // compare
+ const res = await api.readMany("posts", {
+ select: ["title", "count"],
+ });
+ expect(res.map((p) => p.count)).toEqual([1, 1, 1, 2]);
+ }
+ });
+
+ it("refines", async () => {
+ const schema = proto.em({
+ posts: proto.entity("posts", { title: proto.text() }),
+ });
+ const em = schemaToEm(schema);
+ await em.schema().sync({ force: true });
+
+ const payload = [{ title: "foo" }, { title: "bar" }, { title: "baz" }];
+ await em.mutator("posts").insertMany(payload);
+
+ const ctx: any = { em, guard: new Guard() };
+ const controller = new DataController(ctx, dataConfig);
+ const app = controller.getController();
+
+ const api = new DataApi({ basepath: "/" }, app.request as typeof fetch);
+ const normalOne = api.readOne("posts", 1);
+ const normal = api.readMany("posts", { select: ["title"], where: { title: "baz" } });
+ expect((await normal).data).toEqual([{ title: "baz" }] as any);
+
+ // refine
+ const refined = normal.refine((data) => data[0]);
+ expect((await refined).data).toEqual({ title: "baz" } as any);
+
+ // one
+ const oneBy = api.readOneBy("posts", { where: { title: "baz" }, select: ["title"] });
+ const oneByRes = await oneBy;
+ expect(oneByRes.data).toEqual({ title: "baz" } as any);
+ expect(oneByRes.body.meta.count).toEqual(1);
+ });
+
+ it("exists/count", async () => {
+ const schema = proto.em({
+ posts: proto.entity("posts", { title: proto.text() }),
+ });
+ const em = schemaToEm(schema);
+ await em.schema().sync({ force: true });
+
+ const payload = [{ title: "foo" }, { title: "bar" }, { title: "baz" }];
+ await em.mutator("posts").insertMany(payload);
+
+ const ctx: any = { em, guard: new Guard() };
+ const controller = new DataController(ctx, dataConfig);
+ const app = controller.getController();
+
+ const api = new DataApi({ basepath: "/" }, app.request as typeof fetch);
+
+ const exists = api.exists("posts", { id: 1 });
+ expect((await exists).exists).toBeTrue();
+
+ expect((await api.count("posts")).count).toEqual(3);
+ });
+
+ it("creates many", async () => {
+ const schema = proto.em({
+ posts: proto.entity("posts", { title: proto.text(), count: proto.number() }),
+ });
+ const em = schemaToEm(schema);
+ await em.schema().sync({ force: true });
+
+ const payload = [
+ { title: "foo", count: 0 },
+ { title: "bar", count: 0 },
+ { title: "baz", count: 0 },
+ { title: "bla", count: 2 },
+ ];
+
+ const ctx: any = { em, guard: new Guard() };
+ const controller = new DataController(ctx, dataConfig);
+ const app = controller.getController();
+
+ // @ts-ignore tests
+ const api = new DataApi({ basepath: "/" }, app.request as typeof fetch);
+
+ {
+ // create many
+ const res = await api.createMany("posts", payload);
+ expect(res.data.length).toEqual(4);
+ expect(res.ok).toBeTrue();
+ }
+
+ {
+ const req = api.readMany("posts", {
+ select: ["title", "count"],
+ });
+ const res = await req;
+ expect(res.data).toEqual(payload as any);
+ }
+
+ {
+ // create with empty
+ expect(() => api.createMany("posts", [])).toThrow();
}
});
});
diff --git a/app/__test__/api/MediaApi.spec.ts b/app/__test__/api/MediaApi.spec.ts
index 27e86da..a479d19 100644
--- a/app/__test__/api/MediaApi.spec.ts
+++ b/app/__test__/api/MediaApi.spec.ts
@@ -18,8 +18,8 @@ const mockedBackend = new Hono()
return new Response(file, {
headers: {
"Content-Type": file.type,
- "Content-Length": file.size.toString()
- }
+ "Content-Length": file.size.toString(),
+ },
});
});
@@ -30,15 +30,15 @@ describe("MediaApi", () => {
// @ts-ignore tests
const api = new MediaApi({
host,
- basepath
+ basepath,
});
- expect(api.getFileUploadUrl({ path: "path" })).toBe(`${host}${basepath}/upload/path`);
+ expect(api.getFileUploadUrl({ path: "path" } as any)).toBe(`${host}${basepath}/upload/path`);
});
it("should have correct upload headers", () => {
// @ts-ignore tests
const api = new MediaApi({
- token: "token"
+ token: "token",
});
expect(api.getUploadHeaders().get("Authorization")).toBe("Bearer token");
});
@@ -139,7 +139,7 @@ describe("MediaApi", () => {
const response = (await mockedBackend.request(url)) as Response;
await matches(
await api.upload(response.body!, { filename: "readable.png" }),
- "readable.png"
+ "readable.png",
);
}
});
diff --git a/app/__test__/api/ModuleApi.spec.ts b/app/__test__/api/ModuleApi.spec.ts
index 9577fd1..6f65567 100644
--- a/app/__test__/api/ModuleApi.spec.ts
+++ b/app/__test__/api/ModuleApi.spec.ts
@@ -61,7 +61,7 @@ describe("ModuleApi", () => {
it("adds additional headers from options", () => {
const headers = new Headers({
- "X-Test": "123"
+ "X-Test": "123",
});
const api = new Api({ host, headers });
expect(api.get("/").request.headers.get("X-Test")).toEqual("123");
@@ -75,7 +75,7 @@ describe("ModuleApi", () => {
it("uses search params", () => {
const api = new Api({ host });
const search = new URLSearchParams({
- foo: "bar"
+ foo: "bar",
});
expect(api.get("/", search).request.url).toEqual("http://localhost/?" + search.toString());
});
@@ -89,6 +89,14 @@ describe("ModuleApi", () => {
expect(api.delete("/").request.method).toEqual("DELETE");
});
+ it("refines", async () => {
+ const app = new Hono().get("/endpoint", (c) => c.json({ foo: ["bar"] }));
+ const api = new Api({ host }, app.request as typeof fetch);
+
+ expect((await api.get("/endpoint")).data).toEqual({ foo: ["bar"] });
+ expect((await api.get("/endpoint").refine((data) => data.foo)).data).toEqual(["bar"]);
+ });
+
// @todo: test error response
// @todo: test method shortcut functions
});
diff --git a/app/__test__/app/App.spec.ts b/app/__test__/app/App.spec.ts
new file mode 100644
index 0000000..c5e9794
--- /dev/null
+++ b/app/__test__/app/App.spec.ts
@@ -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");
+ });
+});
diff --git a/app/__test__/app/repro.spec.ts b/app/__test__/app/repro.spec.ts
index c76d55f..7b69376 100644
--- a/app/__test__/app/repro.spec.ts
+++ b/app/__test__/app/repro.spec.ts
@@ -24,9 +24,9 @@ describe("repros", async () => {
adapter: {
type: "local",
config: {
- path: "./"
- }
- }
+ path: "./",
+ },
+ },
});
expect(config.enabled).toBe(true);
@@ -38,9 +38,9 @@ describe("repros", async () => {
"entities.test",
proto
.entity("test", {
- content: proto.text()
+ content: proto.text(),
})
- .toJSON()
+ .toJSON(),
);
expect(app.em.entities.map((e) => e.name)).toContain("test");
}
@@ -54,8 +54,8 @@ describe("repros", async () => {
hidden: false,
mime_types: [],
virtual: true,
- entity: "test"
- }
+ entity: "test",
+ },
});
expect(
@@ -63,8 +63,8 @@ describe("repros", async () => {
type: "poly",
source: "test",
target: "media",
- config: { mappedBy: "files" }
- })
+ config: { mappedBy: "files" },
+ }),
).resolves.toBeDefined();
}
@@ -75,17 +75,17 @@ describe("repros", async () => {
const schema = proto.em(
{
products: proto.entity("products", {
- title: proto.text()
+ title: proto.text(),
}),
product_likes: proto.entity("product_likes", {
- created_at: proto.date()
+ created_at: proto.date(),
}),
- users: proto.entity("users", {})
+ users: proto.entity("users", {}),
},
(fns, schema) => {
fns.relation(schema.product_likes).manyToOne(schema.products, { inversedBy: "likes" });
fns.relation(schema.product_likes).manyToOne(schema.users);
- }
+ },
);
const app = createApp({ initialConfig: { data: schema.toJSON() } });
await app.build();
@@ -96,8 +96,8 @@ describe("repros", async () => {
expect(info.relations.listable).toEqual([
{
entity: "product_likes",
- ref: "likes"
- }
+ ref: "likes",
+ },
]);
});
});
diff --git a/app/__test__/auth/authorize/authorize.spec.ts b/app/__test__/auth/authorize/authorize.spec.ts
index 4ca0ce1..4534969 100644
--- a/app/__test__/auth/authorize/authorize.spec.ts
+++ b/app/__test__/auth/authorize/authorize.spec.ts
@@ -7,13 +7,13 @@ describe("authorize", () => {
["read", "write"],
{
admin: {
- permissions: ["read", "write"]
- }
+ permissions: ["read", "write"],
+ },
},
- { enabled: true }
+ { enabled: true },
);
const user = {
- role: "admin"
+ role: "admin",
};
expect(guard.granted("read", user)).toBe(true);
@@ -27,21 +27,21 @@ describe("authorize", () => {
["read", "write"],
{
admin: {
- permissions: ["read", "write"]
+ permissions: ["read", "write"],
},
guest: {
permissions: ["read"],
- is_default: true
- }
+ is_default: true,
+ },
},
- { enabled: true }
+ { enabled: true },
);
expect(guard.granted("read")).toBe(true);
expect(guard.granted("write")).toBe(false);
const user = {
- role: "admin"
+ role: "admin",
};
expect(guard.granted("read", user)).toBe(true);
@@ -58,12 +58,12 @@ describe("authorize", () => {
test("role implicit allow", async () => {
const guard = Guard.create(["read", "write"], {
admin: {
- implicit_allow: true
- }
+ implicit_allow: true,
+ },
});
const user = {
- role: "admin"
+ role: "admin",
};
expect(guard.granted("read", user)).toBe(true);
@@ -74,8 +74,8 @@ describe("authorize", () => {
const guard = Guard.create(["read", "write"], {
guest: {
implicit_allow: true,
- is_default: true
- }
+ is_default: true,
+ },
});
expect(guard.getUserRole()?.name).toBe("guest");
diff --git a/app/__test__/auth/strategies/OAuthStrategy.spec.ts b/app/__test__/auth/strategies/OAuthStrategy.spec.ts
index a41f189..93ceae0 100644
--- a/app/__test__/auth/strategies/OAuthStrategy.spec.ts
+++ b/app/__test__/auth/strategies/OAuthStrategy.spec.ts
@@ -7,10 +7,10 @@ describe("OAuthStrategy", async () => {
const strategy = new OAuthStrategy({
type: "oidc",
client: {
- client_id: process.env.OAUTH_CLIENT_ID,
- client_secret: process.env.OAUTH_CLIENT_SECRET
+ client_id: process.env.OAUTH_CLIENT_ID!,
+ client_secret: process.env.OAUTH_CLIENT_SECRET!,
},
- name: "google"
+ name: "google",
});
const state = "---";
const redirect_uri = "http://localhost:3000/auth/google/callback";
@@ -19,11 +19,6 @@ describe("OAuthStrategy", async () => {
const config = await strategy.getConfig();
console.log("config", JSON.stringify(config, null, 2));
- const request = await strategy.request({
- redirect_uri,
- state
- });
-
const server = Bun.serve({
fetch: async (req) => {
const url = new URL(req.url);
@@ -31,13 +26,18 @@ describe("OAuthStrategy", async () => {
console.log("req", req);
const user = await strategy.callback(url, {
redirect_uri,
- state
+ state,
});
console.log("---user", user);
}
return new Response("Bun!");
- }
+ },
+ });
+
+ const request = await strategy.request({
+ redirect_uri,
+ state,
});
console.log("request", request);
diff --git a/app/__test__/core/EventManager.spec.ts b/app/__test__/core/EventManager.spec.ts
index 5d81e77..995ebfa 100644
--- a/app/__test__/core/EventManager.spec.ts
+++ b/app/__test__/core/EventManager.spec.ts
@@ -26,7 +26,7 @@ class ReturnEvent extends Event<{ foo: string }, string> {
}
return this.clone({
- foo: [this.params.foo, value].join("-")
+ foo: [this.params.foo, value].join("-"),
});
}
}
@@ -52,7 +52,7 @@ describe("EventManager", async () => {
await new Promise((resolve) => setTimeout(resolve, 50));
delayed();
},
- "sync"
+ "sync",
);
// don't allow unknown
@@ -83,8 +83,8 @@ describe("EventManager", async () => {
const emgr = new EventManager(
{ InformationalEvent },
{
- asyncExecutor
- }
+ asyncExecutor,
+ },
);
emgr.onEvent(InformationalEvent, async () => {});
@@ -98,8 +98,8 @@ describe("EventManager", async () => {
const emgr = new EventManager(
{ ReturnEvent, InformationalEvent },
{
- onInvalidReturn
- }
+ onInvalidReturn,
+ },
);
// @ts-expect-error InformationalEvent has no return value
@@ -140,7 +140,7 @@ describe("EventManager", async () => {
expect(slug).toBe("informational-event");
call();
},
- { mode: "sync", once: true }
+ { mode: "sync", once: true },
);
expect(emgr.getListeners().length).toBe(1);
diff --git a/app/__test__/core/Registry.spec.ts b/app/__test__/core/Registry.spec.ts
index 557b39a..637a836 100644
--- a/app/__test__/core/Registry.spec.ts
+++ b/app/__test__/core/Registry.spec.ts
@@ -30,8 +30,8 @@ describe("Registry", () => {
first: {
cls: What,
schema: Type.Object({ type: Type.String(), what: Type.String() }),
- enabled: true
- }
+ enabled: true,
+ },
} satisfies Record);
const item = registry.get("first");
@@ -42,7 +42,7 @@ describe("Registry", () => {
registry.add("second", {
cls: What2,
schema: second,
- enabled: true
+ enabled: true,
});
// @ts-ignore
expect(registry.get("second").schema).toEqual(second);
@@ -52,7 +52,7 @@ describe("Registry", () => {
// @ts-expect-error
cls: NotAllowed,
schema: third,
- enabled: true
+ enabled: true,
});
// @ts-ignore
expect(registry.get("third").schema).toEqual(third);
@@ -62,7 +62,7 @@ describe("Registry", () => {
cls: What,
// @ts-expect-error
schema: fourth,
- enabled: true
+ enabled: true,
});
// @ts-ignore
expect(registry.get("fourth").schema).toEqual(fourth);
@@ -75,7 +75,7 @@ describe("Registry", () => {
return {
cls: a,
schema: a.prototype.getType(),
- enabled: true
+ enabled: true,
};
});
diff --git a/app/__test__/core/cache/CloudflareKvCache.native-spec.ts b/app/__test__/core/cache/CloudflareKvCache.native-spec.ts
index 360bc7d..d5f0812 100644
--- a/app/__test__/core/cache/CloudflareKvCache.native-spec.ts
+++ b/app/__test__/core/cache/CloudflareKvCache.native-spec.ts
@@ -4,7 +4,7 @@ import { after, beforeEach, describe, test } from "node:test";
import { Miniflare } from "miniflare";
import {
CloudflareKVCacheItem,
- CloudflareKVCachePool
+ CloudflareKVCachePool,
} from "../../../src/core/cache/adapters/CloudflareKvCache";
import { runTests } from "./cache-test-suite";
@@ -26,7 +26,7 @@ describe("CloudflareKv", async () => {
mf = new Miniflare({
modules: true,
script: "export default { async fetch() { return new Response(null); } }",
- kvNamespaces: ["TEST"]
+ kvNamespaces: ["TEST"],
});
const kv = await mf.getKVNamespace("TEST");
return new CloudflareKVCachePool(kv as any);
@@ -45,10 +45,10 @@ describe("CloudflareKv", async () => {
},
toBeUndefined() {
assert.equal(actual, undefined);
- }
+ },
};
- }
- }
+ },
+ },
});
after(async () => {
diff --git a/app/__test__/core/cache/MemoryCache.spec.ts b/app/__test__/core/cache/MemoryCache.spec.ts
index 051663a..d78a5d1 100644
--- a/app/__test__/core/cache/MemoryCache.spec.ts
+++ b/app/__test__/core/cache/MemoryCache.spec.ts
@@ -9,7 +9,7 @@ describe("MemoryCache", () => {
tester: {
test,
beforeEach,
- expect
- }
+ expect,
+ },
});
});
diff --git a/app/__test__/core/crypto.spec.ts b/app/__test__/core/crypto.spec.ts
index 238b9e4..3edbcd4 100644
--- a/app/__test__/core/crypto.spec.ts
+++ b/app/__test__/core/crypto.spec.ts
@@ -4,7 +4,7 @@ import { checksum, hash } from "../../src/core/utils";
describe("crypto", async () => {
test("sha256", async () => {
expect(await hash.sha256("test")).toBe(
- "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
+ "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
);
});
test("sha1", async () => {
diff --git a/app/__test__/core/env.spec.ts b/app/__test__/core/env.spec.ts
new file mode 100644
index 0000000..d4c5ba3
--- /dev/null
+++ b/app/__test__/core/env.spec.ts
@@ -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();
+ });
+});
diff --git a/app/__test__/core/object/SchemaObject.spec.ts b/app/__test__/core/object/SchemaObject.spec.ts
index 01db1c7..0c745e4 100644
--- a/app/__test__/core/object/SchemaObject.spec.ts
+++ b/app/__test__/core/object/SchemaObject.spec.ts
@@ -8,8 +8,8 @@ describe("SchemaObject", async () => {
Type.Object({ a: Type.String({ default: "b" }) }),
{ a: "test" },
{
- forceParse: true
- }
+ forceParse: true,
+ },
);
expect(m.get()).toEqual({ a: "test" });
@@ -30,14 +30,14 @@ describe("SchemaObject", async () => {
b: Type.Object(
{
c: Type.String({ default: "d" }),
- e: Type.String({ default: "f" })
+ e: Type.String({ default: "f" }),
},
- { default: {} }
- )
+ { default: {} },
+ ),
},
- { default: {}, additionalProperties: false }
- )
- })
+ { default: {}, additionalProperties: false },
+ ),
+ }),
);
expect(m.get()).toEqual({ s: { a: "b", b: { c: "d", e: "f" } } });
@@ -59,8 +59,8 @@ describe("SchemaObject", async () => {
test("patch array", async () => {
const m = new SchemaObject(
Type.Object({
- methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] })
- })
+ methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
+ }),
);
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
@@ -81,14 +81,14 @@ describe("SchemaObject", async () => {
a: Type.String({ default: "b" }),
b: Type.Object(
{
- c: Type.String({ default: "d" })
+ c: Type.String({ default: "d" }),
},
- { default: {} }
- )
+ { default: {} },
+ ),
},
- { default: {} }
- )
- })
+ { default: {} },
+ ),
+ }),
);
expect(m.get()).toEqual({ s: { a: "b", b: { c: "d" } } });
@@ -108,8 +108,8 @@ describe("SchemaObject", async () => {
test("set", async () => {
const m = new SchemaObject(
Type.Object({
- methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] })
- })
+ methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
+ }),
);
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
@@ -125,7 +125,7 @@ describe("SchemaObject", async () => {
let result: any;
const m = new SchemaObject(
Type.Object({
- methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] })
+ methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
}),
undefined,
{
@@ -133,8 +133,8 @@ describe("SchemaObject", async () => {
await new Promise((r) => setTimeout(r, 10));
called = true;
result = config;
- }
- }
+ },
+ },
);
await m.set({ methods: ["GET", "POST"] });
@@ -146,7 +146,7 @@ describe("SchemaObject", async () => {
let called = false;
const m = new SchemaObject(
Type.Object({
- methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] })
+ methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
}),
undefined,
{
@@ -155,8 +155,8 @@ describe("SchemaObject", async () => {
called = true;
to.methods.push("OPTIONS");
return to;
- }
- }
+ },
+ },
);
const result = await m.set({ methods: ["GET", "POST"] });
@@ -168,7 +168,7 @@ describe("SchemaObject", async () => {
test("throwIfRestricted", async () => {
const m = new SchemaObject(Type.Object({}), undefined, {
- restrictPaths: ["a.b"]
+ restrictPaths: ["a.b"],
});
expect(() => m.throwIfRestricted("a.b")).toThrow();
@@ -185,18 +185,18 @@ describe("SchemaObject", async () => {
a: Type.String({ default: "b" }),
b: Type.Object(
{
- c: Type.String({ default: "d" })
+ c: Type.String({ default: "d" }),
},
- { default: {} }
- )
+ { default: {} },
+ ),
},
- { default: {} }
- )
+ { default: {} },
+ ),
}),
undefined,
{
- restrictPaths: ["s.b"]
- }
+ restrictPaths: ["s.b"],
+ },
);
expect(m.patch("s.b.c", "e")).rejects.toThrow();
@@ -217,33 +217,33 @@ describe("SchemaObject", async () => {
additionalProperties: Type.Object({
type: Type.String(),
config: Type.Optional(
- Type.Object({}, { additionalProperties: Type.String() })
- )
- })
- }
+ Type.Object({}, { additionalProperties: Type.String() }),
+ ),
+ }),
+ },
),
- config: Type.Optional(Type.Object({}, { additionalProperties: Type.String() }))
- })
- }
- )
+ config: Type.Optional(Type.Object({}, { additionalProperties: Type.String() })),
+ }),
+ },
+ ),
},
{
- additionalProperties: false
- }
+ additionalProperties: false,
+ },
);
test("patch safe object, overwrite", async () => {
const data = {
entities: {
some: {
fields: {
- a: { type: "string", config: { some: "thing" } }
- }
- }
- }
+ a: { type: "string", config: { some: "thing" } },
+ },
+ },
+ },
};
const m = new SchemaObject(dataEntitiesSchema, data, {
forceParse: true,
- overwritePaths: [/^entities\..*\.fields\..*\.config/]
+ overwritePaths: [/^entities\..*\.fields\..*\.config/],
});
await m.patch("entities.some.fields.a", { type: "string", config: { another: "one" } });
@@ -252,10 +252,10 @@ describe("SchemaObject", async () => {
entities: {
some: {
fields: {
- a: { type: "string", config: { another: "one" } }
- }
- }
- }
+ a: { type: "string", config: { another: "one" } },
+ },
+ },
+ },
});
});
@@ -265,22 +265,22 @@ describe("SchemaObject", async () => {
users: {
fields: {
email: { type: "string" },
- password: { type: "string" }
- }
- }
- }
+ password: { type: "string" },
+ },
+ },
+ },
};
const m = new SchemaObject(dataEntitiesSchema, data, {
forceParse: true,
- overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/]
+ overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/],
});
await m.patch("entities.test", {
fields: {
content: {
- type: "text"
- }
- }
+ type: "text",
+ },
+ },
});
expect(m.get()).toEqual({
@@ -288,17 +288,17 @@ describe("SchemaObject", async () => {
users: {
fields: {
email: { type: "string" },
- password: { type: "string" }
- }
+ password: { type: "string" },
+ },
},
test: {
fields: {
content: {
- type: "text"
- }
- }
- }
- }
+ type: "text",
+ },
+ },
+ },
+ },
});
});
@@ -308,14 +308,14 @@ describe("SchemaObject", async () => {
users: {
fields: {
email: { type: "string" },
- password: { type: "string" }
- }
- }
- }
+ password: { type: "string" },
+ },
+ },
+ },
};
const m = new SchemaObject(dataEntitiesSchema, data, {
forceParse: true,
- overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/]
+ overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/],
});
expect(m.patch("desc", "entities.users.config.sort_dir")).rejects.toThrow();
@@ -323,13 +323,13 @@ describe("SchemaObject", async () => {
await m.patch("entities.test", {
fields: {
content: {
- type: "text"
- }
- }
+ type: "text",
+ },
+ },
});
await m.patch("entities.users.config", {
- sort_dir: "desc"
+ sort_dir: "desc",
});
expect(m.get()).toEqual({
@@ -337,20 +337,20 @@ describe("SchemaObject", async () => {
users: {
fields: {
email: { type: "string" },
- password: { type: "string" }
+ password: { type: "string" },
},
config: {
- sort_dir: "desc"
- }
+ sort_dir: "desc",
+ },
},
test: {
fields: {
content: {
- type: "text"
- }
- }
- }
- }
+ type: "text",
+ },
+ },
+ },
+ },
});
});
});
diff --git a/app/__test__/core/object/diff.test.ts b/app/__test__/core/object/diff.test.ts
index b6ac6e4..d753880 100644
--- a/app/__test__/core/object/diff.test.ts
+++ b/app/__test__/core/object/diff.test.ts
@@ -13,8 +13,8 @@ describe("diff", () => {
t: "a",
p: ["b"],
o: undefined,
- n: 2
- }
+ n: 2,
+ },
]);
const appliedObj = apply(oldObj, diffs);
@@ -35,8 +35,8 @@ describe("diff", () => {
t: "r",
p: ["b"],
o: 2,
- n: undefined
- }
+ n: undefined,
+ },
]);
const appliedObj = apply(oldObj, diffs);
@@ -57,8 +57,8 @@ describe("diff", () => {
t: "e",
p: ["a"],
o: 1,
- n: 2
- }
+ n: 2,
+ },
]);
const appliedObj = apply(oldObj, diffs);
@@ -79,8 +79,8 @@ describe("diff", () => {
t: "e",
p: ["a", "b"],
o: 1,
- n: 2
- }
+ n: 2,
+ },
]);
const appliedObj = apply(oldObj, diffs);
@@ -101,14 +101,14 @@ describe("diff", () => {
t: "e",
p: ["a", 1],
o: 2,
- n: 4
+ n: 4,
},
{
t: "a",
p: ["a", 3],
o: undefined,
- n: 5
- }
+ n: 5,
+ },
]);
const appliedObj = apply(oldObj, diffs);
@@ -129,20 +129,20 @@ describe("diff", () => {
t: "a",
p: ["a", 0],
o: undefined,
- n: 1
+ n: 1,
},
{
t: "a",
p: ["a", 1],
o: undefined,
- n: 2
+ n: 2,
},
{
t: "a",
p: ["a", 2],
o: undefined,
- n: 3
- }
+ n: 3,
+ },
]);
const appliedObj = apply(oldObj, diffs);
@@ -163,14 +163,14 @@ describe("diff", () => {
t: "e",
p: ["a", 1],
o: 2,
- n: 3
+ n: 3,
},
{
t: "r",
p: ["a", 2],
o: 3,
- n: undefined
- }
+ n: undefined,
+ },
]);
const appliedObj = apply(oldObj, diffs);
@@ -183,14 +183,14 @@ describe("diff", () => {
it("should handle complex nested changes", () => {
const oldObj = {
a: {
- b: [1, 2, { c: 3 }]
- }
+ b: [1, 2, { c: 3 }],
+ },
};
const newObj = {
a: {
- b: [1, 2, { c: 4 }, 5]
- }
+ b: [1, 2, { c: 4 }, 5],
+ },
};
const diffs = diff(oldObj, newObj);
@@ -200,14 +200,14 @@ describe("diff", () => {
t: "e",
p: ["a", "b", 2, "c"],
o: 3,
- n: 4
+ n: 4,
},
{
t: "a",
p: ["a", "b", 3],
o: undefined,
- n: 5
- }
+ n: 5,
+ },
]);
const appliedObj = apply(oldObj, diffs);
@@ -228,14 +228,14 @@ describe("diff", () => {
t: "e",
p: ["a"],
o: undefined,
- n: null
+ n: null,
},
{
t: "e",
p: ["b"],
o: null,
- n: undefined
- }
+ n: undefined,
+ },
]);
const appliedObj = apply(oldObj, diffs);
@@ -256,8 +256,8 @@ describe("diff", () => {
t: "e",
p: ["a"],
o: 1,
- n: "1"
- }
+ n: "1",
+ },
]);
const appliedObj = apply(oldObj, diffs);
@@ -278,14 +278,14 @@ describe("diff", () => {
t: "r",
p: ["b"],
o: 2,
- n: undefined
+ n: undefined,
},
{
t: "a",
p: ["c"],
o: undefined,
- n: 3
- }
+ n: 3,
+ },
]);
const appliedObj = apply(oldObj, diffs);
@@ -306,8 +306,8 @@ describe("diff", () => {
t: "e",
p: ["a"],
o: [1, 2, 3],
- n: { b: 4 }
- }
+ n: { b: 4 },
+ },
]);
const appliedObj = apply(oldObj, diffs);
@@ -328,8 +328,8 @@ describe("diff", () => {
t: "e",
p: ["a"],
o: { b: 1 },
- n: 2
- }
+ n: 2,
+ },
]);
const appliedObj = apply(oldObj, diffs);
@@ -350,14 +350,14 @@ describe("diff", () => {
t: "r",
p: ["a"],
o: 1,
- n: undefined
+ n: undefined,
},
{
t: "a",
p: ["b"],
o: undefined,
- n: 2
- }
+ n: 2,
+ },
]);
const appliedObj = apply(oldObj, diffs);
@@ -408,8 +408,8 @@ describe("diff", () => {
t: "a",
p: ["a"],
o: undefined,
- n: 1
- }
+ n: 1,
+ },
]);
const appliedObj = apply(oldObj, diffs);
@@ -430,8 +430,8 @@ describe("diff", () => {
t: "r",
p: ["a"],
o: 1,
- n: undefined
- }
+ n: undefined,
+ },
]);
const appliedObj = apply(oldObj, diffs);
diff --git a/app/__test__/core/object/object-query.spec.ts b/app/__test__/core/object/object-query.spec.ts
index a6940a9..70ea70c 100644
--- a/app/__test__/core/object/object-query.spec.ts
+++ b/app/__test__/core/object/object-query.spec.ts
@@ -9,7 +9,7 @@ describe("object-query", () => {
test("validates", async () => {
const converted = convert({
- name: { $eq: "ch" }
+ name: { $eq: "ch" },
});
validate(converted, { name: "Michael" });
});
@@ -31,7 +31,7 @@ describe("object-query", () => {
[{ val: { $notnull: 1 } }, { val: null }, false],
[{ val: { $regex: ".*" } }, { val: "test" }, true],
[{ val: { $regex: /^t.*/ } }, { val: "test" }, true],
- [{ val: { $regex: /^b.*/ } }, { val: "test" }, false]
+ [{ val: { $regex: /^b.*/ } }, { val: "test" }, false],
];
for (const [query, object, expected] of tests) {
@@ -55,10 +55,10 @@ describe("object-query", () => {
[
{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } },
{ val1: "foo", val2: "bar" },
- true
+ true,
],
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, { val1: 1 }, true],
- [{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, { val1: 3 }, false]
+ [{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, { val1: 3 }, false],
];
for (const [query, object, expected] of tests) {
diff --git a/app/__test__/core/utils.spec.ts b/app/__test__/core/utils.spec.ts
index 3de8a48..db3c967 100644
--- a/app/__test__/core/utils.spec.ts
+++ b/app/__test__/core/utils.spec.ts
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
-import { Perf, isBlob, ucFirst } from "../../src/core/utils";
+import { Perf, datetimeStringUTC, isBlob, ucFirst } from "../../src/core/utils";
import * as utils from "../../src/core/utils";
async function wait(ms: number) {
@@ -16,7 +16,7 @@ describe("Core Utils", async () => {
expect(result).toEqual([
{ key: "a", value: 1 },
{ key: "b", value: 2 },
- { key: "c", value: 3 }
+ { key: "c", value: 3 },
]);
});
@@ -51,7 +51,7 @@ describe("Core Utils", async () => {
const obj = utils.headersToObject(headers);
expect(obj).toEqual({
"content-type": "application/json",
- authorization: "Bearer 123"
+ authorization: "Bearer 123",
});
});
@@ -82,7 +82,7 @@ describe("Core Utils", async () => {
file: new File([""], "file.txt"),
stream: new ReadableStream(),
arrayBuffer: new ArrayBuffer(10),
- arrayBufferView: new Uint8Array(new ArrayBuffer(10))
+ arrayBufferView: new Uint8Array(new ArrayBuffer(10)),
};
const fns = [
@@ -90,7 +90,7 @@ describe("Core Utils", async () => {
[utils.isBlob, "blob", ["stream", "arrayBuffer", "arrayBufferView"]],
[utils.isFile, "file", ["stream", "arrayBuffer", "arrayBufferView"]],
[utils.isArrayBuffer, "arrayBuffer"],
- [utils.isArrayBufferView, "arrayBufferView"]
+ [utils.isArrayBufferView, "arrayBufferView"],
] as const;
const additional = [0, 0.0, "", null, undefined, {}, []];
@@ -116,10 +116,10 @@ describe("Core Utils", async () => {
const name = "test.json";
const text = "attachment; filename=" + name;
const headers = new Headers({
- "Content-Disposition": text
+ "Content-Disposition": text,
});
const request = new Request("http://example.com", {
- headers
+ headers,
});
expect(utils.getContentName(text)).toBe(name);
@@ -166,7 +166,7 @@ describe("Core Utils", async () => {
[{ a: 1, b: 2, c: 3 }, ["b"], { a: 1, c: 3 }],
[{ a: 1, b: 2, c: 3 }, ["c"], { a: 1, b: 2 }],
[{ a: 1, b: 2, c: 3 }, ["a", "b"], { c: 3 }],
- [{ a: 1, b: 2, c: 3 }, ["a", "b", "c"], {}]
+ [{ a: 1, b: 2, c: 3 }, ["a", "b", "c"], {}],
] as [object, string[], object][];
for (const [obj, keys, expected] of objects) {
@@ -197,9 +197,9 @@ describe("Core Utils", async () => {
new Map([["a", 1]]),
new Map([
["a", 1],
- ["b", 2]
+ ["b", 2],
]),
- false
+ false,
],
[{ a: 1 }, { a: 1 }, true],
[{ a: 1 }, { a: 2 }, false],
@@ -220,7 +220,7 @@ describe("Core Utils", async () => {
[[1, 2, 3], [1, 2, 3, 4], false],
[[{ a: 1 }], [{ a: 1 }], true],
[[{ a: 1 }], [{ a: 2 }], false],
- [[{ a: 1 }], [{ b: 1 }], false]
+ [[{ a: 1 }], [{ b: 1 }], false],
] as [any, any, boolean][];
for (const [a, b, expected] of objects) {
@@ -236,7 +236,7 @@ describe("Core Utils", async () => {
[{ a: { b: 1 } }, "a.b", 1],
[{ a: { b: 1 } }, "a.b.c", null, null],
[{ a: { b: 1 } }, "a.b.c", 1, 1],
- [[[1]], "0.0", 1]
+ [[[1]], "0.0", 1],
] as [object, string, any, any][];
for (const [obj, path, expected, defaultValue] of tests) {
@@ -245,4 +245,14 @@ describe("Core Utils", async () => {
}
});
});
+
+ describe("dates", () => {
+ test.only("formats local time", () => {
+ expect(utils.datetimeStringUTC("2025-02-21T16:48:25.841Z")).toBe("2025-02-21 16:48:25");
+ console.log(utils.datetimeStringUTC(new Date()));
+ console.log(utils.datetimeStringUTC());
+ console.log(new Date());
+ console.log("timezone", Intl.DateTimeFormat().resolvedOptions().timeZone);
+ });
+ });
});
diff --git a/app/__test__/data/DataController.spec.ts b/app/__test__/data/DataController.spec.ts
index cec2022..21ae226 100644
--- a/app/__test__/data/DataController.spec.ts
+++ b/app/__test__/data/DataController.spec.ts
@@ -9,7 +9,7 @@ import {
ManyToOneRelation,
type MutatorResponse,
type RepositoryResponse,
- TextField
+ TextField,
} from "../../src/data";
import { DataController } from "../../src/data/api/DataController";
import { dataConfigSchema } from "../../src/data/data-schema";
@@ -35,17 +35,17 @@ describe("[data] DataController", async () => {
meta: {
total: 0,
count: 0,
- items: 0
- }
+ items: 0,
+ },
});
expect(res).toEqual({
meta: {
total: 0,
count: 0,
- items: 0
+ items: 0,
},
- data: []
+ data: [],
});
});
@@ -59,22 +59,22 @@ describe("[data] DataController", async () => {
data: [] as any,
sql: "",
parameters: [] as any,
- result: [] as any
+ result: [] as any,
});
expect(res).toEqual({
- data: []
+ data: [],
});
});
describe("getController", async () => {
const users = new Entity("users", [
new TextField("name", { required: true }),
- new TextField("bio")
+ new TextField("bio"),
]);
const posts = new Entity("posts", [new TextField("content")]);
const em = new EntityManager([users, posts], dummyConnection, [
- new ManyToOneRelation(posts, users)
+ new ManyToOneRelation(posts, users),
]);
await em.schema().sync({ force: true });
@@ -83,12 +83,12 @@ describe("[data] DataController", async () => {
users: [
{ name: "foo", bio: "bar" },
{ name: "bar", bio: null },
- { name: "baz", bio: "!!!" }
+ { name: "baz", bio: "!!!" },
],
posts: [
{ content: "post 1", users_id: 1 },
- { content: "post 2", users_id: 2 }
- ]
+ { content: "post 2", users_id: 2 },
+ ],
};
const ctx: any = { em, guard: new Guard() };
@@ -118,7 +118,7 @@ describe("[data] DataController", async () => {
for await (const _user of fixtures.users) {
const res = await app.request("/entity/users", {
method: "POST",
- body: JSON.stringify(_user)
+ body: JSON.stringify(_user),
});
//console.log("res", { _user }, res);
const result = (await res.json()) as MutatorResponse;
@@ -133,7 +133,7 @@ describe("[data] DataController", async () => {
for await (const _post of fixtures.posts) {
const res = await app.request("/entity/posts", {
method: "POST",
- body: JSON.stringify(_post)
+ body: JSON.stringify(_post),
});
const result = (await res.json()) as MutatorResponse;
const { id, ...data } = result.data as any;
@@ -159,11 +159,11 @@ describe("[data] DataController", async () => {
const res = await app.request("/entity/users/query", {
method: "POST",
headers: {
- "Content-Type": "application/json"
+ "Content-Type": "application/json",
},
body: JSON.stringify({
- where: { bio: { $isnull: 1 } }
- })
+ where: { bio: { $isnull: 1 } },
+ }),
});
const data = (await res.json()) as RepositoryResponse;
@@ -199,7 +199,7 @@ describe("[data] DataController", async () => {
test("/:entity (update one)", async () => {
const res = await app.request("/entity/users/3", {
method: "PATCH",
- body: JSON.stringify({ name: "new name" })
+ body: JSON.stringify({ name: "new name" }),
});
const { data } = (await res.json()) as MutatorResponse;
@@ -221,7 +221,7 @@ describe("[data] DataController", async () => {
test("/:entity/:id (delete one)", async () => {
const res = await app.request("/entity/posts/2", {
- method: "DELETE"
+ method: "DELETE",
});
const { data } = (await res.json()) as RepositoryResponse;
expect(data).toEqual({ id: 2, ...fixtures.posts[1] });
diff --git a/app/__test__/data/data-query-impl.spec.ts b/app/__test__/data/data-query-impl.spec.ts
index d219bcc..d96247e 100644
--- a/app/__test__/data/data-query-impl.spec.ts
+++ b/app/__test__/data/data-query-impl.spec.ts
@@ -30,7 +30,7 @@ describe("data-query-impl", () => {
[{ val: { $isnull: 0 } }, '"val" is not null', []],
[{ val: { $isnull: false } }, '"val" is not null', []],
[{ val: { $like: "what" } }, '"val" like ?', ["what"]],
- [{ val: { $like: "w*t" } }, '"val" like ?', ["w%t"]]
+ [{ val: { $like: "w*t" } }, '"val" like ?', ["w%t"]],
];
for (const [query, expectedSql, expectedParams] of tests) {
@@ -51,22 +51,22 @@ describe("data-query-impl", () => {
[
{ val1: { $eq: "foo" }, val2: { $eq: "bar" } },
'("val1" = ? and "val2" = ?)',
- ["foo", "bar"]
+ ["foo", "bar"],
],
[
{ val1: { $eq: "foo" }, val2: { $eq: "bar" } },
'("val1" = ? and "val2" = ?)',
- ["foo", "bar"]
+ ["foo", "bar"],
],
// or constructs
[
{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } },
'("val1" = ? or "val2" = ?)',
- ["foo", "bar"]
+ ["foo", "bar"],
],
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, '("val1" = ? or "val1" = ?)', [1, 2]],
- [{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, '("val1" = ? or "val1" = ?)', [1, 2]]
+ [{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, '("val1" = ? or "val1" = ?)', [1, 2]],
];
for (const [query, expectedSql, expectedParams] of tests) {
@@ -86,7 +86,7 @@ describe("data-query-impl", () => {
// or constructs
[{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } }, ["val1", "val2"]],
- [{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, ["val1"]]
+ [{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, ["val1"]],
];
for (const [query, expectedKeys] of tests) {
@@ -105,23 +105,23 @@ describe("data-query-impl", () => {
posts: {
with: {
images: {
- select: ["id"]
- }
- }
- }
- }
+ select: ["id"],
+ },
+ },
+ },
+ },
},
{
with: {
posts: {
with: {
images: {
- select: ["id"]
- }
- }
- }
- }
- }
+ select: ["id"],
+ },
+ },
+ },
+ },
+ },
);
// over http
diff --git a/app/__test__/data/data.test.ts b/app/__test__/data/data.test.ts
index dc8257f..886f2aa 100644
--- a/app/__test__/data/data.test.ts
+++ b/app/__test__/data/data.test.ts
@@ -5,7 +5,7 @@ import {
NumberField,
PrimaryField,
Repository,
- TextField
+ TextField,
} from "../../src/data";
import { getDummyConnection } from "./helper";
@@ -18,14 +18,14 @@ describe("some tests", async () => {
const users = new Entity("users", [
new TextField("username", { required: true, default_value: "nobody" }),
- new TextField("email", { maxLength: 3 })
+ new TextField("email", { maxLength: 3 }),
]);
const posts = new Entity("posts", [
new TextField("title"),
new TextField("content"),
new TextField("created_at"),
- new NumberField("likes", { default_value: 0 })
+ new NumberField("likes", { default_value: 0 }),
]);
const em = new EntityManager([users, posts], connection);
@@ -43,7 +43,7 @@ describe("some tests", async () => {
});*/
expect(query.sql).toBe(
- 'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" where "id" = ? limit ?'
+ 'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" where "id" = ? limit ?',
);
expect(query.parameters).toEqual([1, 1]);
expect(query.result).toEqual([]);
@@ -53,7 +53,7 @@ describe("some tests", async () => {
const query = await em.repository(users).findMany();
expect(query.sql).toBe(
- 'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" order by "users"."id" asc limit ? offset ?'
+ 'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" order by "users"."id" asc limit ? offset ?',
);
expect(query.parameters).toEqual([10, 0]);
expect(query.result).toEqual([]);
@@ -63,7 +63,7 @@ describe("some tests", async () => {
const query = await em.repository(posts).findMany();
expect(query.sql).toBe(
- 'select "posts"."id" as "id", "posts"."title" as "title", "posts"."content" as "content", "posts"."created_at" as "created_at", "posts"."likes" as "likes" from "posts" order by "posts"."id" asc limit ? offset ?'
+ 'select "posts"."id" as "id", "posts"."title" as "title", "posts"."content" as "content", "posts"."created_at" as "created_at", "posts"."likes" as "likes" from "posts" order by "posts"."id" asc limit ? offset ?',
);
expect(query.parameters).toEqual([10, 0]);
expect(query.result).toEqual([]);
@@ -74,7 +74,7 @@ describe("some tests", async () => {
new Entity("users", [
new TextField("username"),
new TextField("email"),
- new TextField("email") // not throwing, it's just being ignored
+ new TextField("email"), // not throwing, it's just being ignored
]);
}).toBeDefined();
@@ -83,7 +83,7 @@ describe("some tests", async () => {
new TextField("username"),
new TextField("email"),
// field config differs, will throw
- new TextField("email", { required: true })
+ new TextField("email", { required: true }),
]);
}).toThrow();
@@ -91,7 +91,7 @@ describe("some tests", async () => {
new Entity("users", [
new PrimaryField(),
new TextField("username"),
- new TextField("email")
+ new TextField("email"),
]);
}).toBeDefined();
});
diff --git a/app/__test__/data/helper.ts b/app/__test__/data/helper.ts
index dc4465b..df1ed88 100644
--- a/app/__test__/data/helper.ts
+++ b/app/__test__/data/helper.ts
@@ -16,7 +16,7 @@ export function getDummyDatabase(memory: boolean = true): {
afterAllCleanup: async () => {
if (!memory) await unlink(DB_NAME);
return true;
- }
+ },
};
}
@@ -26,7 +26,7 @@ export function getDummyConnection(memory: boolean = true) {
return {
dummyConnection,
- afterAllCleanup
+ afterAllCleanup,
};
}
diff --git a/app/__test__/data/mutation.relation.test.ts b/app/__test__/data/mutation.relation.test.ts
index 32b8749..57124b2 100644
--- a/app/__test__/data/mutation.relation.test.ts
+++ b/app/__test__/data/mutation.relation.test.ts
@@ -6,7 +6,7 @@ import {
ManyToOneRelation,
NumberField,
SchemaManager,
- TextField
+ TextField,
} from "../../src/data";
import { getDummyConnection } from "./helper";
@@ -21,7 +21,7 @@ describe("Mutator relation", async () => {
const posts = new Entity("posts", [
new TextField("title"),
new TextField("content", { default_value: "..." }),
- new NumberField("count", { default_value: 0 })
+ new NumberField("count", { default_value: 0 }),
]);
const users = new Entity("users", [new TextField("username")]);
@@ -44,7 +44,7 @@ describe("Mutator relation", async () => {
expect(em.mutator(posts).insertOne({ title: "post2", users_id: 10 })).rejects.toThrow();
expect(
- em.mutator(posts).insertOne({ title: "post2", users_id: data.id })
+ em.mutator(posts).insertOne({ title: "post2", users_id: data.id }),
).resolves.toBeDefined();
});
});
diff --git a/app/__test__/data/mutation.simple.test.ts b/app/__test__/data/mutation.simple.test.ts
index ac19935..f425733 100644
--- a/app/__test__/data/mutation.simple.test.ts
+++ b/app/__test__/data/mutation.simple.test.ts
@@ -14,7 +14,7 @@ describe("Mutator simple", async () => {
const items = new Entity("items", [
new TextField("label", { required: true, minLength: 1 }),
- new NumberField("count", { default_value: 0 })
+ new NumberField("count", { default_value: 0 }),
]);
const em = new EntityManager([items], connection);
@@ -29,11 +29,11 @@ describe("Mutator simple", async () => {
test("insert single row", async () => {
const mutation = await em.mutator(items).insertOne({
label: "test",
- count: 1
+ count: 1,
});
expect(mutation.sql).toBe(
- 'insert into "items" ("count", "label") values (?, ?) returning "id", "label", "count"'
+ 'insert into "items" ("count", "label") values (?, ?) returning "id", "label", "count"',
);
expect(mutation.data).toEqual({ id: 1, label: "test", count: 1 });
@@ -41,8 +41,8 @@ describe("Mutator simple", async () => {
limit: 1,
sort: {
by: "id",
- dir: "desc"
- }
+ dir: "desc",
+ },
});
expect(query.result).toEqual([{ id: 1, label: "test", count: 1 }]);
@@ -53,18 +53,18 @@ describe("Mutator simple", async () => {
limit: 1,
sort: {
by: "id",
- dir: "desc"
- }
+ dir: "desc",
+ },
});
const id = query.data![0].id as number;
const mutation = await em.mutator(items).updateOne(id, {
label: "new label",
- count: 100
+ count: 100,
});
expect(mutation.sql).toBe(
- 'update "items" set "label" = ?, "count" = ? where "id" = ? returning "id", "label", "count"'
+ 'update "items" set "label" = ?, "count" = ? where "id" = ? returning "id", "label", "count"',
);
expect(mutation.data).toEqual({ id, label: "new label", count: 100 });
});
@@ -74,15 +74,15 @@ describe("Mutator simple", async () => {
limit: 1,
sort: {
by: "id",
- dir: "desc"
- }
+ dir: "desc",
+ },
});
const id = query.data![0].id as number;
const mutation = await em.mutator(items).deleteOne(id);
expect(mutation.sql).toBe(
- 'delete from "items" where "id" = ? returning "id", "label", "count"'
+ 'delete from "items" where "id" = ? returning "id", "label", "count"',
);
expect(mutation.data).toEqual({ id, label: "new label", count: 100 });
@@ -94,7 +94,7 @@ describe("Mutator simple", async () => {
const incompleteCreate = async () =>
await em.mutator(items).insertOne({
//label: "test",
- count: 1
+ count: 1,
});
expect(incompleteCreate()).rejects.toThrow();
@@ -104,7 +104,7 @@ describe("Mutator simple", async () => {
const invalidCreate1 = async () =>
await em.mutator(items).insertOne({
label: 111, // this should work
- count: "1" // this should fail
+ count: "1", // this should fail
});
expect(invalidCreate1()).rejects.toThrow(TransformPersistFailedException);
@@ -112,7 +112,7 @@ describe("Mutator simple", async () => {
const invalidCreate2 = async () =>
await em.mutator(items).insertOne({
label: "", // this should fail
- count: 1
+ count: 1,
});
expect(invalidCreate2()).rejects.toThrow(TransformPersistFailedException);
@@ -137,7 +137,7 @@ describe("Mutator simple", async () => {
expect((await em.repository(items).findMany()).data.length).toBe(data.length - 2);
//console.log((await em.repository(items).findMany()).data);
- await em.mutator(items).deleteWhere();
+ await em.mutator(items).deleteWhere({ id: { $isnull: 0 } });
expect((await em.repository(items).findMany()).data.length).toBe(0);
//expect(res.data.count).toBe(0);
@@ -152,27 +152,27 @@ describe("Mutator simple", async () => {
await em.mutator(items).updateWhere(
{ count: 2 },
{
- count: 10
- }
+ count: 10,
+ },
);
expect((await em.repository(items).findMany()).data).toEqual([
{ id: 6, label: "update", count: 1 },
{ id: 7, label: "update too", count: 1 },
- { id: 8, label: "keep", count: 0 }
+ { id: 8, label: "keep", count: 0 },
]);
// expect 2 to be updated
await em.mutator(items).updateWhere(
{ count: 2 },
{
- count: 1
- }
+ count: 1,
+ },
);
expect((await em.repository(items).findMany()).data).toEqual([
{ id: 6, label: "update", count: 2 },
{ id: 7, label: "update too", count: 2 },
- { id: 8, label: "keep", count: 0 }
+ { id: 8, label: "keep", count: 0 },
]);
});
diff --git a/app/__test__/data/polymorphic.test.ts b/app/__test__/data/polymorphic.test.ts
index 44df2de..88a0b8b 100644
--- a/app/__test__/data/polymorphic.test.ts
+++ b/app/__test__/data/polymorphic.test.ts
@@ -23,23 +23,23 @@ describe("Polymorphic", async () => {
source: "categories",
target: "media",
config: {
- mappedBy: "image"
- }
+ mappedBy: "image",
+ },
});
// media should not see categories
expect(em.relationsOf(media.name).map((r) => r.toJSON())).toEqual([]);
// it's important that media cannot access categories
expect(em.relations.targetRelationsOf(categories).map((r) => r.source.entity.name)).toEqual(
- []
+ [],
);
expect(em.relations.targetRelationsOf(media).map((r) => r.source.entity.name)).toEqual([]);
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.entity.name)).toEqual([
- "media"
+ "media",
]);
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.reference)).toEqual([
- "image"
+ "image",
]);
expect(em.relations.sourceRelationsOf(media).map((r) => r.target.entity.name)).toEqual([]);
@@ -48,7 +48,7 @@ describe("Polymorphic", async () => {
"id",
"path",
"reference",
- "entity_id"
+ "entity_id",
]);
expect(media.getSelect()).toEqual(["id", "path"]);
});
@@ -60,7 +60,7 @@ describe("Polymorphic", async () => {
const entities = [media, categories];
const single = new PolymorphicRelation(categories, media, {
mappedBy: "single",
- targetCardinality: 1
+ targetCardinality: 1,
});
const multiple = new PolymorphicRelation(categories, media, { mappedBy: "multiple" });
@@ -71,17 +71,17 @@ describe("Polymorphic", async () => {
// it's important that media cannot access categories
expect(em.relations.targetRelationsOf(categories).map((r) => r.source.entity.name)).toEqual(
- []
+ [],
);
expect(em.relations.targetRelationsOf(media).map((r) => r.source.entity.name)).toEqual([]);
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.entity.name)).toEqual([
"media",
- "media"
+ "media",
]);
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.reference)).toEqual([
"single",
- "multiple"
+ "multiple",
]);
expect(em.relations.sourceRelationsOf(media).map((r) => r.target.entity.name)).toEqual([]);
@@ -90,7 +90,7 @@ describe("Polymorphic", async () => {
"id",
"path",
"reference",
- "entity_id"
+ "entity_id",
]);
});
});
diff --git a/app/__test__/data/prototype.test.ts b/app/__test__/data/prototype.test.ts
index 8b12aa4..83f0de1 100644
--- a/app/__test__/data/prototype.test.ts
+++ b/app/__test__/data/prototype.test.ts
@@ -12,7 +12,7 @@ import {
NumberField,
OneToOneRelation,
PolymorphicRelation,
- TextField
+ TextField,
} from "../../src/data";
import { DummyConnection } from "../../src/data/connection/DummyConnection";
import {
@@ -31,7 +31,7 @@ import {
medium,
number,
relation,
- text
+ text,
} from "../../src/data/prototype";
import { MediaField } from "../../src/media/MediaField";
@@ -54,7 +54,7 @@ describe("prototype", () => {
name: text(),
bio: text(),
age: number(),
- some: number()
+ some: number(),
});
type db = {
users: Schema;
@@ -70,7 +70,7 @@ describe("prototype", () => {
name: text({ default_value: "hello" }).required(),
bio: text(),
age: number(),
- some: number().required()
+ some: number().required(),
});
const obj: InsertSchema = { name: "yo", some: 1 };
@@ -83,12 +83,12 @@ describe("prototype", () => {
new TextField("title", { required: true }),
new TextField("content"),
new DateField("created_at", {
- type: "datetime"
+ type: "datetime",
}),
// @ts-ignore
new MediaField("images", { entity: "posts" }),
// @ts-ignore
- new MediaField("cover", { entity: "posts", max_items: 1 })
+ new MediaField("cover", { entity: "posts", max_items: 1 }),
]);
const posts2 = entity("posts", {
@@ -96,7 +96,7 @@ describe("prototype", () => {
content: text(),
created_at: datetime(),
images: media(),
- cover: medium()
+ cover: medium(),
});
type Posts = Schema;
@@ -117,11 +117,11 @@ describe("prototype", () => {
type: "objects",
values: [
{ value: "active", label: "Active" },
- { value: "inactive", label: "Not active" }
- ]
- }
+ { value: "inactive", label: "Not active" },
+ ],
+ },
}),
- new JsonField("json")
+ new JsonField("json"),
]);
const test2 = entity("test", {
@@ -134,10 +134,10 @@ describe("prototype", () => {
status: enumm<"active" | "inactive">({
enum: [
{ value: "active", label: "Active" },
- { value: "inactive", label: "Not active" }
- ]
+ { value: "inactive", label: "Not active" },
+ ],
}),
- json: json<{ some: number }>()
+ json: json<{ some: number }>(),
});
expect(test.toJSON()).toEqual(test2.toJSON());
@@ -161,12 +161,12 @@ describe("prototype", () => {
// category has single image
new PolymorphicRelation(categories, _media, {
mappedBy: "image",
- targetCardinality: 1
+ targetCardinality: 1,
}),
// post has multiple images
new PolymorphicRelation(posts, _media, { mappedBy: "images" }),
- new PolymorphicRelation(posts, _media, { mappedBy: "cover", targetCardinality: 1 })
+ new PolymorphicRelation(posts, _media, { mappedBy: "cover", targetCardinality: 1 }),
];
const relations2 = [
@@ -180,7 +180,7 @@ describe("prototype", () => {
relation(categories).polyToOne(_media, { mappedBy: "image" }),
relation(posts).polyToMany(_media, { mappedBy: "images" }),
- relation(posts).polyToOne(_media, { mappedBy: "cover" })
+ relation(posts).polyToOne(_media, { mappedBy: "cover" }),
];
expect(relations.map((r) => r.toJSON())).toEqual(relations2.map((r) => r.toJSON()));
@@ -194,21 +194,21 @@ describe("prototype", () => {
posts,
categories,
{
- connectionTableMappedName: "custom"
+ connectionTableMappedName: "custom",
},
- [new TextField("description")]
+ [new TextField("description")],
);
const fields = {
- description: text()
+ description: text(),
};
let o: FieldSchema;
const rel2 = relation(posts).manyToMany(
categories,
{
- connectionTableMappedName: "custom"
+ connectionTableMappedName: "custom",
},
- fields
+ fields,
);
expect(rel.toJSON()).toEqual(rel2.toJSON());
@@ -216,11 +216,11 @@ describe("prototype", () => {
test("devexample", async () => {
const users = entity("users", {
- username: text()
+ username: text(),
});
const comments = entity("comments", {
- content: text()
+ content: text(),
});
const posts = entity("posts", {
@@ -228,17 +228,17 @@ describe("prototype", () => {
content: text(),
created_at: datetime(),
images: media(),
- cover: medium()
+ cover: medium(),
});
const categories = entity("categories", {
name: text(),
description: text(),
- image: medium()
+ image: medium(),
});
const settings = entity("settings", {
- theme: text()
+ theme: text(),
});
const test = entity("test", {
@@ -251,10 +251,10 @@ describe("prototype", () => {
status: enumm<"active" | "inactive">({
enum: [
{ value: "active", label: "Active" },
- { value: "inactive", label: "Not active" }
- ]
+ { value: "inactive", label: "Not active" },
+ ],
}),
- json: json<{ some: number }>()
+ json: json<{ some: number }>(),
});
const _media = entity("media", {});
@@ -270,7 +270,7 @@ describe("prototype", () => {
relation(users).oneToOne(settings),
relation(comments).manyToOne(users, { required: true }),
- relation(comments).manyToOne(posts, { required: true })
+ relation(comments).manyToOne(posts, { required: true }),
];
const obj: Schema = {} as any;
@@ -281,12 +281,12 @@ describe("prototype", () => {
{
posts: entity("posts", { name: text(), slug: text().required() }),
comments: entity("comments", { some: text() }),
- users: entity("users", { email: text() })
+ users: entity("users", { email: text() }),
},
({ relation, index }, { posts, comments, users }) => {
relation(posts).manyToOne(comments).manyToOne(users);
index(posts).on(["name"]).on(["slug"], true);
- }
+ },
);
type LocalDb = (typeof _em)["DB"];
@@ -294,7 +294,7 @@ describe("prototype", () => {
const es = [
new Entity("posts", [new TextField("name"), new TextField("slug", { required: true })]),
new Entity("comments", [new TextField("some")]),
- new Entity("users", [new TextField("email")])
+ new Entity("users", [new TextField("email")]),
];
const _em2 = new EntityManager(
es,
@@ -302,8 +302,8 @@ describe("prototype", () => {
[new ManyToOneRelation(es[0], es[1]), new ManyToOneRelation(es[0], es[2])],
[
new EntityIndex(es[0], [es[0].field("name")!]),
- new EntityIndex(es[0], [es[0].field("slug")!], true)
- ]
+ new EntityIndex(es[0], [es[0].field("slug")!], true),
+ ],
);
// @ts-ignore
diff --git a/app/__test__/data/relations.test.ts b/app/__test__/data/relations.test.ts
index 19eab85..b4ff708 100644
--- a/app/__test__/data/relations.test.ts
+++ b/app/__test__/data/relations.test.ts
@@ -6,7 +6,7 @@ import {
ManyToOneRelation,
OneToOneRelation,
PolymorphicRelation,
- RelationField
+ RelationField,
} from "../../src/data/relations";
import { getDummyConnection } from "./helper";
@@ -22,7 +22,7 @@ describe("Relations", async () => {
const r1 = new RelationField("users_id", {
reference: "users",
target: "users",
- target_field: "id"
+ target_field: "id",
});
const sql1 = schema
@@ -31,14 +31,14 @@ describe("Relations", async () => {
.compile().sql;
expect(sql1).toBe(
- 'create table "posts" ("users_id" integer references "users" ("id") on delete set null)'
+ 'create table "posts" ("users_id" integer references "users" ("id") on delete set null)',
);
//const r2 = new RelationField(new Entity("users"), "author");
const r2 = new RelationField("author_id", {
reference: "author",
target: "users",
- target_field: "id"
+ target_field: "id",
});
const sql2 = schema
@@ -47,7 +47,7 @@ describe("Relations", async () => {
.compile().sql;
expect(sql2).toBe(
- 'create table "posts" ("author_id" integer references "users" ("id") on delete set null)'
+ 'create table "posts" ("author_id" integer references "users" ("id") on delete set null)',
);
});
@@ -57,7 +57,7 @@ describe("Relations", async () => {
reference: "users",
target: "users",
target_field: "id",
- required: true
+ required: true,
});
expect(r1.isRequired()).toBeTrue();
});
@@ -66,8 +66,8 @@ describe("Relations", async () => {
const users = new Entity("users", [new TextField("username")]);
const posts = new Entity("posts", [
new TextField("title", {
- maxLength: 2
- })
+ maxLength: 2,
+ }),
]);
const entities = [users, posts];
@@ -122,7 +122,7 @@ describe("Relations", async () => {
.selectFrom(users.name)
.select((eb) => postAuthorRel.buildWith(users, "posts")(eb).as("posts"));
expect(selectPostsFromUsers.compile().sql).toBe(
- 'select (select from "posts" as "posts" where "posts"."author_id" = "users"."id") as "posts" from "users"'
+ 'select (select from "posts" as "posts" where "posts"."author_id" = "users"."id") as "posts" from "users"',
);
expect(postAuthorRel!.getField()).toBeInstanceOf(RelationField);
const userObj = { id: 1, username: "test" };
@@ -142,7 +142,7 @@ describe("Relations", async () => {
.select((eb) => postAuthorRel.buildWith(posts, "author")(eb).as("author"));
expect(selectUsersFromPosts.compile().sql).toBe(
- 'select (select from "users" as "author" where "author"."id" = "posts"."author_id" limit ?) as "author" from "posts"'
+ 'select (select from "users" as "author" where "author"."id" = "posts"."author_id" limit ?) as "author" from "posts"',
);
expect(postAuthorRel.getField()).toBeInstanceOf(RelationField);
const postObj = { id: 1, title: "test" };
@@ -158,7 +158,7 @@ describe("Relations", async () => {
$detach: false,
primary: undefined,
cardinality: undefined,
- relation_type: "n:1"
+ relation_type: "n:1",
});
expect(postAuthorRel!.helper(posts.name)!.getMutationInfo()).toEqual({
@@ -170,7 +170,7 @@ describe("Relations", async () => {
$detach: false,
primary: "id",
cardinality: 1,
- relation_type: "n:1"
+ relation_type: "n:1",
});
/*console.log("ManyToOne (source=posts, target=users)");
@@ -225,7 +225,7 @@ describe("Relations", async () => {
$detach: false,
primary: "id",
cardinality: 1,
- relation_type: "1:1"
+ relation_type: "1:1",
});
expect(userSettingRel!.helper(settings.name)!.getMutationInfo()).toEqual({
reference: "users",
@@ -236,7 +236,7 @@ describe("Relations", async () => {
$detach: false,
primary: undefined,
cardinality: 1,
- relation_type: "1:1"
+ relation_type: "1:1",
});
/*console.log("");
@@ -310,16 +310,16 @@ describe("Relations", async () => {
*/
const selectCategoriesFromPosts = kysely
.selectFrom(posts.name)
- .select((eb) => postCategoriesRel.buildWith(posts)(eb).as("categories"));
+ .select((eb) => postCategoriesRel.buildWith(posts)(eb).select("id").as("categories"));
expect(selectCategoriesFromPosts.compile().sql).toBe(
- 'select (select "categories"."id" as "id", "categories"."label" as "label" from "categories" inner join "posts_categories" on "categories"."id" = "posts_categories"."categories_id" where "posts"."id" = "posts_categories"."posts_id" limit ?) as "categories" from "posts"'
+ 'select (select "id" from "categories" inner join "posts_categories" on "categories"."id" = "posts_categories"."categories_id" where "posts"."id" = "posts_categories"."posts_id" limit ?) as "categories" from "posts"',
);
const selectPostsFromCategories = kysely
.selectFrom(categories.name)
- .select((eb) => postCategoriesRel.buildWith(categories)(eb).as("posts"));
+ .select((eb) => postCategoriesRel.buildWith(categories)(eb).select("id").as("posts"));
expect(selectPostsFromCategories.compile().sql).toBe(
- 'select (select "posts"."id" as "id", "posts"."title" as "title" from "posts" inner join "posts_categories" on "posts"."id" = "posts_categories"."posts_id" where "categories"."id" = "posts_categories"."categories_id" limit ?) as "posts" from "categories"'
+ 'select (select "id" from "posts" inner join "posts_categories" on "posts"."id" = "posts_categories"."posts_id" where "categories"."id" = "posts_categories"."categories_id" limit ?) as "posts" from "categories"',
);
// mutation info
@@ -332,7 +332,7 @@ describe("Relations", async () => {
$detach: true,
primary: "id",
cardinality: undefined,
- relation_type: "m:n"
+ relation_type: "m:n",
});
expect(relations[0].helper(categories.name)!.getMutationInfo()).toEqual({
reference: "posts",
@@ -343,7 +343,7 @@ describe("Relations", async () => {
$detach: false,
primary: undefined,
cardinality: undefined,
- relation_type: "m:n"
+ relation_type: "m:n",
});
/*console.log("");
diff --git a/app/__test__/data/specs/Entity.spec.ts b/app/__test__/data/specs/Entity.spec.ts
index 9b66b96..358c106 100644
--- a/app/__test__/data/specs/Entity.spec.ts
+++ b/app/__test__/data/specs/Entity.spec.ts
@@ -6,7 +6,7 @@ describe("[data] Entity", async () => {
new TextField("name", { required: true }),
new TextField("description"),
new NumberField("age", { fillable: false, default_value: 18 }),
- new TextField("hidden", { hidden: true, default_value: "secret" })
+ new TextField("hidden", { hidden: true, default_value: "secret" }),
]);
test("getSelect", async () => {
@@ -17,7 +17,7 @@ describe("[data] Entity", async () => {
expect(entity.getFillableFields().map((f) => f.name)).toEqual([
"name",
"description",
- "hidden"
+ "hidden",
]);
});
@@ -28,7 +28,7 @@ describe("[data] Entity", async () => {
test("getDefaultObject", async () => {
expect(entity.getDefaultObject()).toEqual({
age: 18,
- hidden: "secret"
+ hidden: "secret",
});
});
diff --git a/app/__test__/data/specs/EntityManager.spec.ts b/app/__test__/data/specs/EntityManager.spec.ts
index 797a419..c9b5dba 100644
--- a/app/__test__/data/specs/EntityManager.spec.ts
+++ b/app/__test__/data/specs/EntityManager.spec.ts
@@ -4,7 +4,7 @@ import {
EntityManager,
ManyToManyRelation,
ManyToOneRelation,
- SchemaManager
+ SchemaManager,
} from "../../../src/data";
import { UnableToConnectException } from "../../../src/data/errors";
import { getDummyConnection } from "../helper";
@@ -25,7 +25,7 @@ describe("[data] EntityManager", async () => {
expect(await em.ping()).toBe(true);
expect(() => em.entity("...")).toThrow();
expect(() =>
- em.addRelation(new ManyToOneRelation(new Entity("1"), new Entity("2")))
+ em.addRelation(new ManyToOneRelation(new Entity("1"), new Entity("2"))),
).toThrow();
expect(em.schema()).toBeInstanceOf(SchemaManager);
@@ -98,7 +98,7 @@ describe("[data] EntityManager", async () => {
expect(userTargetRel.map((r) => r.other(users).entity.name)).toEqual(["posts", "comments"]);
expect(postTargetRel.map((r) => r.other(posts).entity.name)).toEqual([
"comments",
- "categories"
+ "categories",
]);
expect(commentTargetRel.map((r) => r.other(comments).entity.name)).toEqual([]);
expect(categoriesTargetRel.map((r) => r.other(categories).entity.name)).toEqual(["posts"]);
diff --git a/app/__test__/data/specs/JoinBuilder.spec.ts b/app/__test__/data/specs/JoinBuilder.spec.ts
index 37afd08..9260aeb 100644
--- a/app/__test__/data/specs/JoinBuilder.spec.ts
+++ b/app/__test__/data/specs/JoinBuilder.spec.ts
@@ -12,7 +12,7 @@ describe("[data] JoinBuilder", async () => {
const em = new EntityManager([users], dummyConnection);
expect(() =>
- JoinBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, ["posts"])
+ JoinBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, ["posts"]),
).toThrow('Relation "posts" not found');
});
@@ -23,7 +23,7 @@ describe("[data] JoinBuilder", async () => {
const em = new EntityManager([users, posts], dummyConnection, relations);
const qb = JoinBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, [
- "posts"
+ "posts",
]);
const res = qb.compile();
@@ -34,7 +34,7 @@ describe("[data] JoinBuilder", async () => {
);*/
const qb2 = JoinBuilder.addClause(em, em.connection.kysely.selectFrom("posts"), posts, [
- "author"
+ "author",
]);
const res2 = qb2.compile();
diff --git a/app/__test__/data/specs/Mutator.spec.ts b/app/__test__/data/specs/Mutator.spec.ts
index 6493d52..4b3bee7 100644
--- a/app/__test__/data/specs/Mutator.spec.ts
+++ b/app/__test__/data/specs/Mutator.spec.ts
@@ -9,7 +9,7 @@ import {
OneToOneRelation,
type RelationField,
RelationMutator,
- TextField
+ TextField,
} from "../../../src/data";
import * as proto from "../../../src/data/prototype";
import { getDummyConnection } from "../helper";
@@ -22,7 +22,7 @@ describe("[data] Mutator (base)", async () => {
new TextField("label", { required: true }),
new NumberField("count"),
new TextField("hidden", { hidden: true }),
- new TextField("not_fillable", { fillable: false })
+ new TextField("not_fillable", { fillable: false }),
]);
const em = new EntityManager([entity], dummyConnection);
await em.schema().sync({ force: true });
@@ -44,7 +44,7 @@ describe("[data] Mutator (base)", async () => {
test("updateOne", async () => {
const { data } = await em.mutator(entity).insertOne(payload);
const updated = await em.mutator(entity).updateOne(data.id, {
- count: 2
+ count: 2,
});
expect(updated.parameters).toEqual([2, data.id]);
@@ -77,7 +77,7 @@ describe("[data] Mutator (ManyToOne)", async () => {
// persisting relational field should just return key value to be added
expect(
- postRelMutator.persistRelationField(postRelField, "users_id", userData.data.id)
+ postRelMutator.persistRelationField(postRelField, "users_id", userData.data.id),
).resolves.toEqual(["users_id", userData.data.id]);
// persisting invalid value should throw
@@ -86,8 +86,8 @@ describe("[data] Mutator (ManyToOne)", async () => {
// persisting reference should ...
expect(
postRelMutator.persistReference(relations[0]!, "users", {
- $set: { id: userData.data.id }
- })
+ $set: { id: userData.data.id },
+ }),
).resolves.toEqual(["users_id", userData.data.id]);
// @todo: add what methods are allowed to relation, like $create should not be allowed for post<>users
@@ -99,8 +99,8 @@ describe("[data] Mutator (ManyToOne)", async () => {
expect(
em.mutator(posts).insertOne({
title: "post1",
- users_id: 100 // user does not exist yet
- })
+ users_id: 100, // user does not exist yet
+ }),
).rejects.toThrow();
});
@@ -111,7 +111,7 @@ describe("[data] Mutator (ManyToOne)", async () => {
const em = new EntityManager([items, cats], dummyConnection, relations);
expect(em.mutator(items).insertOne({ label: "test" })).rejects.toThrow(
- 'Field "cats_id" is required'
+ 'Field "cats_id" is required',
);
});
@@ -119,14 +119,14 @@ describe("[data] Mutator (ManyToOne)", async () => {
const { data } = await em.mutator(users).insertOne({ username: "user1" });
const res = await em.mutator(posts).insertOne({
title: "post1",
- users_id: data.id
+ users_id: data.id,
});
expect(res.data.users_id).toBe(data.id);
// setting "null" should be allowed
const res2 = await em.mutator(posts).insertOne({
title: "post1",
- users_id: null
+ users_id: null,
});
expect(res2.data.users_id).toBe(null);
});
@@ -135,14 +135,14 @@ describe("[data] Mutator (ManyToOne)", async () => {
const { data } = await em.mutator(users).insertOne({ username: "user1" });
const res = await em.mutator(posts).insertOne({
title: "post1",
- users: { $set: { id: data.id } }
+ users: { $set: { id: data.id } },
});
expect(res.data.users_id).toBe(data.id);
// setting "null" should be allowed
const res2 = await em.mutator(posts).insertOne({
title: "post1",
- users: { $set: { id: null } }
+ users: { $set: { id: null } },
});
expect(res2.data.users_id).toBe(null);
});
@@ -151,8 +151,8 @@ describe("[data] Mutator (ManyToOne)", async () => {
expect(
em.mutator(posts).insertOne({
title: "test",
- users: { $create: { username: "test" } }
- })
+ users: { $create: { username: "test" } },
+ }),
).rejects.toThrow();
});
@@ -162,27 +162,27 @@ describe("[data] Mutator (ManyToOne)", async () => {
const res2 = await em.mutator(posts).insertOne({ title: "post1" });
const up1 = await em.mutator(posts).updateOne(res2.data.id, {
- users: { $set: { id: res1.data.id } }
+ users: { $set: { id: res1.data.id } },
});
expect(up1.data.users_id).toBe(res1.data.id);
const up2 = await em.mutator(posts).updateOne(res2.data.id, {
- users: { $set: { id: res1_1.data.id } }
+ users: { $set: { id: res1_1.data.id } },
});
expect(up2.data.users_id).toBe(res1_1.data.id);
const up3_1 = await em.mutator(posts).updateOne(res2.data.id, {
- users_id: res1.data.id
+ users_id: res1.data.id,
});
expect(up3_1.data.users_id).toBe(res1.data.id);
const up3_2 = await em.mutator(posts).updateOne(res2.data.id, {
- users_id: res1_1.data.id
+ users_id: res1_1.data.id,
});
expect(up3_2.data.users_id).toBe(res1_1.data.id);
const up4 = await em.mutator(posts).updateOne(res2.data.id, {
- users_id: null
+ users_id: null,
});
expect(up4.data.users_id).toBe(null);
});
@@ -199,8 +199,8 @@ describe("[data] Mutator (OneToOne)", async () => {
expect(
em.mutator(users).insertOne({
username: "test",
- settings_id: 1 // todo: throws because it doesn't exist, but it shouldn't be allowed
- })
+ settings_id: 1, // todo: throws because it doesn't exist, but it shouldn't be allowed
+ }),
).rejects.toThrow();
});
@@ -210,15 +210,15 @@ describe("[data] Mutator (OneToOne)", async () => {
expect(
em.mutator(users).insertOne({
username: "test",
- settings: { $set: { id: data.id } }
- })
+ settings: { $set: { id: data.id } },
+ }),
).rejects.toThrow();
});
test("insertOne: using $create", async () => {
const res = await em.mutator(users).insertOne({
username: "test",
- settings: { $create: { theme: "dark" } }
+ settings: { $create: { theme: "dark" } },
});
expect(res.data.settings_id).toBeDefined();
});
@@ -303,7 +303,7 @@ describe("[data] Mutator (Events)", async () => {
test("insertOne event return is respected", async () => {
const posts = proto.entity("posts", {
title: proto.text(),
- views: proto.number()
+ views: proto.number(),
});
const conn = getDummyConnection();
@@ -318,10 +318,10 @@ describe("[data] Mutator (Events)", async () => {
async (event) => {
return {
...event.params.data,
- views: 2
+ views: 2,
};
},
- "sync"
+ "sync",
);
const mutator = em.mutator("posts");
@@ -329,14 +329,14 @@ describe("[data] Mutator (Events)", async () => {
expect(result.data).toEqual({
id: 1,
title: "test",
- views: 2
+ views: 2,
});
});
test("updateOne event return is respected", async () => {
const posts = proto.entity("posts", {
title: proto.text(),
- views: proto.number()
+ views: proto.number(),
});
const conn = getDummyConnection();
@@ -351,10 +351,10 @@ describe("[data] Mutator (Events)", async () => {
async (event) => {
return {
...event.params.data,
- views: event.params.data.views + 1
+ views: event.params.data.views + 1,
};
},
- "sync"
+ "sync",
);
const mutator = em.mutator("posts");
@@ -363,7 +363,7 @@ describe("[data] Mutator (Events)", async () => {
expect(result.data).toEqual({
id: 1,
title: "test",
- views: 3
+ views: 3,
});
});
});
diff --git a/app/__test__/data/specs/Repository.spec.ts b/app/__test__/data/specs/Repository.spec.ts
index d873389..2a42b9e 100644
--- a/app/__test__/data/specs/Repository.spec.ts
+++ b/app/__test__/data/specs/Repository.spec.ts
@@ -7,7 +7,7 @@ import {
LibsqlConnection,
ManyToOneRelation,
RepositoryEvents,
- TextField
+ TextField,
} from "../../../src/data";
import { getDummyConnection } from "../helper";
@@ -70,13 +70,13 @@ describe("[Repository]", async () => {
const q1 = selectQ(conn).compile();
const res = await client.execute({
sql: q1.sql,
- args: q1.parameters as any
+ args: q1.parameters as any,
});
const q2 = countQ(conn).compile();
const count = await client.execute({
sql: q2.sql,
- args: q2.parameters as any
+ args: q2.parameters as any,
});
return [res, count];
}
@@ -93,7 +93,7 @@ describe("[Repository]", async () => {
const exec = async (
name: string,
fn: (em: EntityManager) => Promise,
- em: EntityManager
+ em: EntityManager,
) => {
const res = await Perf.execute(() => fn(em), times);
await sleep(1000);
@@ -102,7 +102,7 @@ describe("[Repository]", async () => {
total: res.total.toFixed(2),
avg: (res.total / times).toFixed(2),
first: res.marks[0].time.toFixed(2),
- last: res.marks[res.marks.length - 1].time.toFixed(2)
+ last: res.marks[res.marks.length - 1].time.toFixed(2),
};
console.log(info.name, info, res.marks);
return info;
@@ -183,7 +183,7 @@ describe("[data] Repository (Events)", async () => {
const items = new Entity("items", [new TextField("label")]);
const categories = new Entity("categories", [new TextField("label")]);
const em = new EntityManager([items, categories], dummyConnection, [
- new ManyToOneRelation(categories, items)
+ new ManyToOneRelation(categories, items),
]);
await em.schema().sync({ force: true });
const events = new Map();
diff --git a/app/__test__/data/specs/SchemaManager.spec.ts b/app/__test__/data/specs/SchemaManager.spec.ts
index 4cfb7d0..9e9fe90 100644
--- a/app/__test__/data/specs/SchemaManager.spec.ts
+++ b/app/__test__/data/specs/SchemaManager.spec.ts
@@ -26,7 +26,7 @@ describe("SchemaManager tests", async () => {
isNullable: true,
isAutoIncrementing: true,
hasDefaultValue: false,
- comment: undefined
+ comment: undefined,
},
{
name: "username",
@@ -34,7 +34,7 @@ describe("SchemaManager tests", async () => {
isNullable: true,
isAutoIncrementing: false,
hasDefaultValue: false,
- comment: undefined
+ comment: undefined,
},
{
name: "email",
@@ -42,7 +42,7 @@ describe("SchemaManager tests", async () => {
isNullable: true,
isAutoIncrementing: false,
hasDefaultValue: false,
- comment: undefined
+ comment: undefined,
},
{
name: "bio",
@@ -50,8 +50,8 @@ describe("SchemaManager tests", async () => {
isNullable: true,
isAutoIncrementing: false,
hasDefaultValue: false,
- comment: undefined
- }
+ comment: undefined,
+ },
],
indices: [
{
@@ -61,11 +61,11 @@ describe("SchemaManager tests", async () => {
columns: [
{
name: "email",
- order: 0
- }
- ]
- }
- ]
+ order: 0,
+ },
+ ],
+ },
+ ],
});
});
@@ -77,10 +77,10 @@ describe("SchemaManager tests", async () => {
new Entity(table, [
new TextField("username"),
new TextField("email"),
- new TextField("bio")
- ])
+ new TextField("bio"),
+ ]),
],
- dummyConnection
+ dummyConnection,
);
const kysely = em.connection.kysely;
@@ -101,8 +101,8 @@ describe("SchemaManager tests", async () => {
name: table,
isNew: false,
columns: { add: ["bio"], drop: [], change: [] },
- indices: { add: [], drop: [index] }
- }
+ indices: { add: [], drop: [index] },
+ },
]);
// now sync
@@ -119,7 +119,7 @@ describe("SchemaManager tests", async () => {
const table = "drop_column";
const em = new EntityManager(
[new Entity(table, [new TextField("username")])],
- dummyConnection
+ dummyConnection,
);
const kysely = em.connection.kysely;
@@ -141,10 +141,10 @@ describe("SchemaManager tests", async () => {
columns: {
add: [],
drop: ["email"],
- change: []
+ change: [],
},
- indices: { add: [], drop: [] }
- }
+ indices: { add: [], drop: [] },
+ },
]);
// now sync
@@ -165,15 +165,15 @@ describe("SchemaManager tests", async () => {
new Entity(usersTable, [
new TextField("username"),
new TextField("email"),
- new TextField("bio")
+ new TextField("bio"),
]),
new Entity(postsTable, [
new TextField("title"),
new TextField("content"),
- new TextField("created_at")
- ])
+ new TextField("created_at"),
+ ]),
],
- dummyConnection
+ dummyConnection,
);
const kysely = em.connection.kysely;
@@ -192,7 +192,7 @@ describe("SchemaManager tests", async () => {
name: usersTable,
isNew: false,
columns: { add: ["bio"], drop: [], change: [] },
- indices: { add: [], drop: [] }
+ indices: { add: [], drop: [] },
},
{
name: postsTable,
@@ -200,10 +200,10 @@ describe("SchemaManager tests", async () => {
columns: {
add: ["id", "title", "content", "created_at"],
drop: [],
- change: []
+ change: [],
},
- indices: { add: [], drop: [] }
- }
+ indices: { add: [], drop: [] },
+ },
]);
// now sync
@@ -228,8 +228,8 @@ describe("SchemaManager tests", async () => {
name: entity.name,
isNew: true,
columns: { add: ["id", "email"], drop: [], change: [] },
- indices: { add: [index.name!], drop: [] }
- }
+ indices: { add: [index.name!], drop: [] },
+ },
]);
// sync and then check again
@@ -256,8 +256,8 @@ describe("SchemaManager tests", async () => {
name: entity.name,
isNew: false,
columns: { add: [], drop: [], change: [] },
- indices: { add: [index.name!], drop: [] }
- }
+ indices: { add: [index.name!], drop: [] },
+ },
]);
// sync and then check again
diff --git a/app/__test__/data/specs/WithBuilder.spec.ts b/app/__test__/data/specs/WithBuilder.spec.ts
index 7b64198..db0273c 100644
--- a/app/__test__/data/specs/WithBuilder.spec.ts
+++ b/app/__test__/data/specs/WithBuilder.spec.ts
@@ -1,5 +1,4 @@
-import { afterAll, describe, expect, test } from "bun:test";
-import { _jsonp } from "../../../src/core/utils";
+import { describe, expect, test } from "bun:test";
import {
Entity,
EntityManager,
@@ -7,10 +6,10 @@ import {
ManyToOneRelation,
PolymorphicRelation,
TextField,
- WithBuilder
+ WithBuilder,
} from "../../../src/data";
import * as proto from "../../../src/data/prototype";
-import { compileQb, prettyPrintQb, schemaToEm } from "../../helper";
+import { schemaToEm } from "../../helper";
import { getDummyConnection } from "../helper";
const { dummyConnection } = getDummyConnection();
@@ -21,32 +20,32 @@ describe("[data] WithBuilder", async () => {
{
posts: proto.entity("posts", {}),
users: proto.entity("users", {}),
- media: proto.entity("media", {})
+ media: proto.entity("media", {}),
},
({ relation }, { posts, users, media }) => {
relation(posts).manyToOne(users);
relation(users).polyToOne(media, { mappedBy: "avatar" });
- }
+ },
);
const em = schemaToEm(schema);
- expect(WithBuilder.validateWiths(em, "posts", undefined)).toBe(0);
+ expect(WithBuilder.validateWiths(em, "posts", undefined as any)).toBe(0);
expect(WithBuilder.validateWiths(em, "posts", {})).toBe(0);
expect(WithBuilder.validateWiths(em, "posts", { users: {} })).toBe(1);
expect(
WithBuilder.validateWiths(em, "posts", {
users: {
- with: { avatar: {} }
- }
- })
+ with: { avatar: {} },
+ },
+ }),
).toBe(2);
expect(() => WithBuilder.validateWiths(em, "posts", { author: {} })).toThrow();
expect(() =>
WithBuilder.validateWiths(em, "posts", {
users: {
- with: { glibberish: {} }
- }
- })
+ with: { glibberish: {} },
+ },
+ }),
).toThrow();
});
@@ -56,8 +55,8 @@ describe("[data] WithBuilder", async () => {
expect(() =>
WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, {
- posts: {}
- })
+ posts: {},
+ }),
).toThrow('Relation "users<>posts" not found');
});
@@ -68,13 +67,13 @@ describe("[data] WithBuilder", async () => {
const em = new EntityManager([users, posts], dummyConnection, relations);
const qb = WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, {
- posts: {}
+ posts: {},
});
const res = qb.compile();
expect(res.sql).toBe(
- 'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'content\', "agg"."content", \'author_id\', "agg"."author_id")), \'[]\') from (select "posts"."id" as "id", "posts"."content" as "content", "posts"."author_id" as "author_id" from "posts" as "posts" where "posts"."author_id" = "users"."id" order by "posts"."id" asc limit ? offset ?) as agg) as "posts" from "users"'
+ 'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'content\', "agg"."content", \'author_id\', "agg"."author_id")), \'[]\') from (select "posts"."id" as "id", "posts"."content" as "content", "posts"."author_id" as "author_id" from "posts" as "posts" where "posts"."author_id" = "users"."id" order by "posts"."id" asc limit ? offset ?) as agg) as "posts" from "users"',
);
expect(res.parameters).toEqual([10, 0]);
@@ -83,14 +82,14 @@ describe("[data] WithBuilder", async () => {
em.connection.kysely.selectFrom("posts"),
posts, // @todo: try with "users", it gives output!
{
- author: {}
- }
+ author: {},
+ },
);
const res2 = qb2.compile();
expect(res2.sql).toBe(
- 'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "users"."id" as "id", "users"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" order by "users"."id" asc limit ? offset ?) as obj) as "author" from "posts"'
+ 'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "users"."id" as "id", "users"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" order by "users"."id" asc limit ? offset ?) as obj) as "author" from "posts"',
);
expect(res2.parameters).toEqual([1, 0]);
});
@@ -124,7 +123,7 @@ describe("[data] WithBuilder", async () => {
.values([
{ posts_id: 1, categories_id: 1 },
{ posts_id: 2, categories_id: 2 },
- { posts_id: 1, categories_id: 2 }
+ { posts_id: 1, categories_id: 2 },
])
.execute();
@@ -138,14 +137,14 @@ describe("[data] WithBuilder", async () => {
title: "fashion post",
categories: [
{ id: 1, label: "fashion" },
- { id: 2, label: "beauty" }
- ]
+ { id: 2, label: "beauty" },
+ ],
},
{
id: 2,
title: "beauty post",
- categories: [{ id: 2, label: "beauty" }]
- }
+ categories: [{ id: 2, label: "beauty" }],
+ },
]);
const res2 = await em.repository(categories).findMany({ with: { posts: {} } });
@@ -156,21 +155,21 @@ describe("[data] WithBuilder", async () => {
{
id: 1,
label: "fashion",
- posts: [{ id: 1, title: "fashion post" }]
+ posts: [{ id: 1, title: "fashion post" }],
},
{
id: 2,
label: "beauty",
posts: [
{ id: 1, title: "fashion post" },
- { id: 2, title: "beauty post" }
- ]
+ { id: 2, title: "beauty post" },
+ ],
},
{
id: 3,
label: "tech",
- posts: []
- }
+ posts: [],
+ },
]);
});
@@ -181,7 +180,7 @@ describe("[data] WithBuilder", async () => {
const entities = [media, categories];
const single = new PolymorphicRelation(categories, media, {
mappedBy: "single",
- targetCardinality: 1
+ targetCardinality: 1,
});
const multiple = new PolymorphicRelation(categories, media, { mappedBy: "multiple" });
@@ -191,11 +190,11 @@ describe("[data] WithBuilder", async () => {
em,
em.connection.kysely.selectFrom("categories"),
categories,
- { single: {} }
+ { single: {} },
);
const res = qb.compile();
expect(res.sql).toBe(
- 'select (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as obj) as "single" from "categories"'
+ 'select (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as obj) as "single" from "categories"',
);
expect(res.parameters).toEqual(["categories.single", 1, 0]);
@@ -203,11 +202,11 @@ describe("[data] WithBuilder", async () => {
em,
em.connection.kysely.selectFrom("categories"),
categories,
- { multiple: {} }
+ { multiple: {} },
);
const res2 = qb2.compile();
expect(res2.sql).toBe(
- 'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'path\', "agg"."path")), \'[]\') from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as agg) as "multiple" from "categories"'
+ 'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'path\', "agg"."path")), \'[]\') from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as agg) as "multiple" from "categories"',
);
expect(res2.parameters).toEqual(["categories.multiple", 10, 0]);
});
@@ -240,16 +239,16 @@ describe("[data] WithBuilder", async () => {
{
posts: proto.entity("posts", {}),
users: proto.entity("users", {
- username: proto.text()
+ username: proto.text(),
}),
media: proto.entity("media", {
- path: proto.text()
- })
+ path: proto.text(),
+ }),
},
({ relation }, { posts, users, media }) => {
relation(posts).manyToOne(users);
relation(users).polyToOne(media, { mappedBy: "avatar" });
- }
+ },
);
const em = schemaToEm(schema);
@@ -265,16 +264,16 @@ describe("[data] WithBuilder", async () => {
with: {
avatar: {
select: ["id", "path"],
- limit: 2 // ignored
- }
- }
- }
- }
+ limit: 2, // ignored
+ },
+ },
+ },
+ },
);
//prettyPrintQb(qb);
expect(qb.compile().sql).toBe(
- 'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username", \'avatar\', "obj"."avatar") from (select "users"."id" as "id", "users"."username" as "username", (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "users"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as obj) as "avatar" from "users" as "users" where "users"."id" = "posts"."users_id" order by "users"."username" asc limit ? offset ?) as obj) as "users" from "posts"'
+ 'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username", \'avatar\', "obj"."avatar") from (select "users"."id" as "id", "users"."username" as "username", (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "users"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as obj) as "avatar" from "users" as "users" where "users"."id" = "posts"."users_id" order by "users"."username" asc limit ? offset ?) as obj) as "users" from "posts"',
);
expect(qb.compile().parameters).toEqual(["users.avatar", 1, 0, 1, 0]);
});
@@ -285,17 +284,17 @@ describe("[data] WithBuilder", async () => {
posts: proto.entity("posts", {}),
comments: proto.entity("comments", {}),
users: proto.entity("users", {
- username: proto.text()
+ username: proto.text(),
}),
media: proto.entity("media", {
- path: proto.text()
- })
+ path: proto.text(),
+ }),
},
({ relation }, { posts, comments, users, media }) => {
relation(posts).manyToOne(users).polyToOne(media, { mappedBy: "images" });
relation(users).polyToOne(media, { mappedBy: "avatar" });
relation(comments).manyToOne(posts).manyToOne(users);
- }
+ },
);
const em = schemaToEm(schema);
@@ -308,15 +307,15 @@ describe("[data] WithBuilder", async () => {
limit: 12,
with: {
users: {
- select: ["username"]
- }
- }
- }
- }
+ select: ["username"],
+ },
+ },
+ },
+ },
);
expect(qb.compile().sql).toBe(
- 'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'posts_id\', "agg"."posts_id", \'users_id\', "agg"."users_id", \'users\', "agg"."users")), \'[]\') from (select "comments"."id" as "id", "comments"."posts_id" as "posts_id", "comments"."users_id" as "users_id", (select json_object(\'username\', "obj"."username") from (select "users"."username" as "username" from "users" as "users" where "users"."id" = "comments"."users_id" order by "users"."id" asc limit ? offset ?) as obj) as "users" from "comments" as "comments" where "comments"."posts_id" = "posts"."id" order by "comments"."id" asc limit ? offset ?) as agg) as "comments" from "posts"'
+ 'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'posts_id\', "agg"."posts_id", \'users_id\', "agg"."users_id", \'users\', "agg"."users")), \'[]\') from (select "comments"."id" as "id", "comments"."posts_id" as "posts_id", "comments"."users_id" as "users_id", (select json_object(\'username\', "obj"."username") from (select "users"."username" as "username" from "users" as "users" where "users"."id" = "comments"."users_id" order by "users"."id" asc limit ? offset ?) as obj) as "users" from "comments" as "comments" where "comments"."posts_id" = "posts"."id" order by "comments"."id" asc limit ? offset ?) as agg) as "comments" from "posts"',
);
expect(qb.compile().parameters).toEqual([1, 0, 12, 0]);
});
@@ -325,23 +324,23 @@ describe("[data] WithBuilder", async () => {
const schema = proto.em(
{
posts: proto.entity("posts", {
- title: proto.text()
+ title: proto.text(),
}),
comments: proto.entity("comments", {
- content: proto.text()
+ content: proto.text(),
}),
users: proto.entity("users", {
- username: proto.text()
+ username: proto.text(),
}),
media: proto.entity("media", {
- path: proto.text()
- })
+ path: proto.text(),
+ }),
},
({ relation }, { posts, comments, users, media }) => {
relation(posts).manyToOne(users).polyToOne(media, { mappedBy: "images" });
relation(users).polyToOne(media, { mappedBy: "avatar" });
relation(comments).manyToOne(posts).manyToOne(users);
- }
+ },
);
const em = schemaToEm(schema);
await em.schema().sync({ force: true });
@@ -351,7 +350,7 @@ describe("[data] WithBuilder", async () => {
await em.mutator("posts").insertMany([
{ title: "post1", users_id: 1 },
{ title: "post2", users_id: 1 },
- { title: "post3", users_id: 2 }
+ { title: "post3", users_id: 2 },
]);
await em.mutator("comments").insertMany([
{ content: "comment1", posts_id: 1, users_id: 1 },
@@ -360,7 +359,7 @@ describe("[data] WithBuilder", async () => {
{ content: "comment3", posts_id: 2, users_id: 1 },
{ content: "comment4", posts_id: 2, users_id: 2 },
{ content: "comment5", posts_id: 3, users_id: 1 },
- { content: "comment6", posts_id: 3, users_id: 2 }
+ { content: "comment6", posts_id: 3, users_id: 2 },
]);
const result = await em.repo("posts").findMany({
@@ -371,11 +370,11 @@ describe("[data] WithBuilder", async () => {
select: ["content"],
with: {
users: {
- select: ["username"]
- }
- }
- }
- }
+ select: ["username"],
+ },
+ },
+ },
+ },
});
expect(result.data).toEqual([
@@ -385,16 +384,16 @@ describe("[data] WithBuilder", async () => {
{
content: "comment1",
users: {
- username: "user1"
- }
+ username: "user1",
+ },
},
{
content: "comment1-1",
users: {
- username: "user1"
- }
- }
- ]
+ username: "user1",
+ },
+ },
+ ],
},
{
title: "post2",
@@ -402,16 +401,16 @@ describe("[data] WithBuilder", async () => {
{
content: "comment3",
users: {
- username: "user1"
- }
+ username: "user1",
+ },
},
{
content: "comment4",
users: {
- username: "user2"
- }
- }
- ]
+ username: "user2",
+ },
+ },
+ ],
},
{
title: "post3",
@@ -419,17 +418,17 @@ describe("[data] WithBuilder", async () => {
{
content: "comment5",
users: {
- username: "user1"
- }
+ username: "user1",
+ },
},
{
content: "comment6",
users: {
- username: "user2"
- }
- }
- ]
- }
+ username: "user2",
+ },
+ },
+ ],
+ },
]);
//console.log(_jsonp(result.data));
});
diff --git a/app/__test__/data/specs/connection/Connection.spec.ts b/app/__test__/data/specs/connection/Connection.spec.ts
index cc73f7b..d3e8226 100644
--- a/app/__test__/data/specs/connection/Connection.spec.ts
+++ b/app/__test__/data/specs/connection/Connection.spec.ts
@@ -22,10 +22,10 @@ describe("Connection", async () => {
columns: [
{
name: "name",
- order: 0
- }
- ]
- }
+ order: 0,
+ },
+ ],
+ },
]);
});
@@ -54,14 +54,14 @@ describe("Connection", async () => {
columns: [
{
name: "name",
- order: 0
+ order: 0,
},
{
name: "desc",
- order: 1
- }
- ]
- }
+ order: 1,
+ },
+ ],
+ },
]);
});
@@ -83,10 +83,10 @@ describe("Connection", async () => {
columns: [
{
name: "name",
- order: 0
- }
- ]
- }
+ order: 0,
+ },
+ ],
+ },
]);
});
});
diff --git a/app/__test__/data/specs/fields/EnumField.spec.ts b/app/__test__/data/specs/fields/EnumField.spec.ts
index 7cde4eb..2187bee 100644
--- a/app/__test__/data/specs/fields/EnumField.spec.ts
+++ b/app/__test__/data/specs/fields/EnumField.spec.ts
@@ -10,12 +10,12 @@ describe("[data] EnumField", async () => {
runBaseFieldTests(
EnumField,
{ defaultValue: "a", schemaType: "text" },
- { options: options(["a", "b", "c"]) }
+ { options: options(["a", "b", "c"]) },
);
test("yields if default value is not a valid option", async () => {
expect(
- () => new EnumField("test", { options: options(["a", "b"]), default_value: "c" })
+ () => new EnumField("test", { options: options(["a", "b"]), default_value: "c" }),
).toThrow();
});
@@ -31,7 +31,7 @@ describe("[data] EnumField", async () => {
const field = new EnumField("test", {
options: options(["a", "b", "c"]),
default_value: "a",
- required: true
+ required: true,
});
expect(field.transformRetrieve(null)).toBe("a");
diff --git a/app/__test__/data/specs/fields/Field.spec.ts b/app/__test__/data/specs/fields/Field.spec.ts
index 77eb3fd..45e4351 100644
--- a/app/__test__/data/specs/fields/Field.spec.ts
+++ b/app/__test__/data/specs/fields/Field.spec.ts
@@ -24,20 +24,20 @@ describe("[data] Field", async () => {
const required = new FieldSpec("test", { required: true });
const requiredDefault = new FieldSpec("test", {
required: true,
- default_value: "test"
+ default_value: "test",
});
expect(required.transformPersist(null, undefined as any, undefined as any)).rejects.toThrow();
expect(
- required.transformPersist(undefined, undefined as any, undefined as any)
+ required.transformPersist(undefined, undefined as any, undefined as any),
).rejects.toThrow();
// works because it has a default value
expect(
- requiredDefault.transformPersist(null, undefined as any, undefined as any)
+ requiredDefault.transformPersist(null, undefined as any, undefined as any),
).resolves.toBeDefined();
expect(
- requiredDefault.transformPersist(undefined, undefined as any, undefined as any)
+ requiredDefault.transformPersist(undefined, undefined as any, undefined as any),
).resolves.toBeDefined();
});
});
diff --git a/app/__test__/data/specs/fields/FieldIndex.spec.ts b/app/__test__/data/specs/fields/FieldIndex.spec.ts
index 3ba9606..8f1590c 100644
--- a/app/__test__/data/specs/fields/FieldIndex.spec.ts
+++ b/app/__test__/data/specs/fields/FieldIndex.spec.ts
@@ -5,7 +5,7 @@ import {
EntityIndex,
type EntityManager,
Field,
- type SchemaResponse
+ type SchemaResponse,
} from "../../../../src/data";
class TestField extends Field {
diff --git a/app/__test__/data/specs/fields/JsonField.spec.ts b/app/__test__/data/specs/fields/JsonField.spec.ts
index 17fdaaa..dff15a1 100644
--- a/app/__test__/data/specs/fields/JsonField.spec.ts
+++ b/app/__test__/data/specs/fields/JsonField.spec.ts
@@ -7,7 +7,7 @@ describe("[data] JsonField", async () => {
runBaseFieldTests(JsonField, {
defaultValue: { a: 1 },
sampleValues: ["string", { test: 1 }, 1],
- schemaType: "text"
+ schemaType: "text",
});
test("transformPersist (no config)", async () => {
diff --git a/app/__test__/data/specs/fields/inc.ts b/app/__test__/data/specs/fields/inc.ts
index 98b5e5f..1754c20 100644
--- a/app/__test__/data/specs/fields/inc.ts
+++ b/app/__test__/data/specs/fields/inc.ts
@@ -18,7 +18,7 @@ export function transformPersist(field: Field, value: any, context?: TActionCont
export function runBaseFieldTests(
fieldClass: ConstructableField,
config: FieldTestConfig,
- _requiredConfig: any = {}
+ _requiredConfig: any = {},
) {
const noConfigField = new fieldClass("no_config", _requiredConfig);
const fillable = new fieldClass("fillable", { ..._requiredConfig, fillable: true });
@@ -29,7 +29,7 @@ export function runBaseFieldTests(
..._requiredConfig,
fillable: true,
required: true,
- default_value: config.defaultValue
+ default_value: config.defaultValue,
});
test("schema", () => {
@@ -37,7 +37,7 @@ export function runBaseFieldTests(
expect(noConfigField.schema(null as any)).toEqual([
"no_config",
config.schemaType,
- expect.any(Function)
+ expect.any(Function),
]);
});
@@ -96,7 +96,7 @@ export function runBaseFieldTests(
//order: 1,
fillable: true,
required: false,
- hidden: false
+ hidden: false,
//virtual: false,
//default_value: undefined
};
@@ -105,20 +105,20 @@ export function runBaseFieldTests(
const json = field.toJSON();
return {
...json,
- config: omit(json.config, ["html"])
+ config: omit(json.config, ["html"]),
};
}
expect(fieldJson(noConfigField)).toEqual({
//name: "no_config",
type: noConfigField.type,
- config: _config
+ config: _config,
});
expect(fieldJson(fillable)).toEqual({
//name: "fillable",
type: noConfigField.type,
- config: _config
+ config: _config,
});
expect(fieldJson(required)).toEqual({
@@ -126,8 +126,8 @@ export function runBaseFieldTests(
type: required.type,
config: {
..._config,
- required: true
- }
+ required: true,
+ },
});
expect(fieldJson(hidden)).toEqual({
@@ -135,8 +135,8 @@ export function runBaseFieldTests(
type: required.type,
config: {
..._config,
- hidden: true
- }
+ hidden: true,
+ },
});
expect(fieldJson(dflt)).toEqual({
@@ -144,8 +144,8 @@ export function runBaseFieldTests(
type: dflt.type,
config: {
..._config,
- default_value: config.defaultValue
- }
+ default_value: config.defaultValue,
+ },
});
expect(fieldJson(requiredAndDefault)).toEqual({
@@ -155,8 +155,8 @@ export function runBaseFieldTests(
..._config,
fillable: true,
required: true,
- default_value: config.defaultValue
- }
+ default_value: config.defaultValue,
+ },
});
});
}
diff --git a/app/__test__/data/specs/relations/EntityRelation.spec.ts b/app/__test__/data/specs/relations/EntityRelation.spec.ts
index 92c50e3..62561b6 100644
--- a/app/__test__/data/specs/relations/EntityRelation.spec.ts
+++ b/app/__test__/data/specs/relations/EntityRelation.spec.ts
@@ -4,7 +4,7 @@ import {
type BaseRelationConfig,
EntityRelation,
EntityRelationAnchor,
- RelationTypes
+ RelationTypes,
} from "../../../../src/data/relations";
class TestEntityRelation extends EntityRelation {
@@ -12,7 +12,7 @@ class TestEntityRelation extends EntityRelation {
super(
new EntityRelationAnchor(new Entity("source"), "source"),
new EntityRelationAnchor(new Entity("target"), "target"),
- config
+ config,
);
}
initialize(em: EntityManager) {}
diff --git a/app/__test__/flows/FetchTask.spec.ts b/app/__test__/flows/FetchTask.spec.ts
index fe8e731..d10bc84 100644
--- a/app/__test__/flows/FetchTask.spec.ts
+++ b/app/__test__/flows/FetchTask.spec.ts
@@ -30,14 +30,14 @@ beforeAll(() =>
method: init?.method ?? "GET",
// @ts-ignore
headers: Object.fromEntries(init?.headers?.entries() ?? []),
- body: init?.body
+ body: init?.body,
};
return new Response(JSON.stringify({ todos: [1, 2], request }), {
status: 200,
- headers: { "Content-Type": "application/json" }
+ headers: { "Content-Type": "application/json" },
});
- })
+ }),
);
afterAll(unmockFetch);
@@ -46,7 +46,7 @@ describe("FetchTask", async () => {
const task = new FetchTask("Fetch Something", {
url: "https://jsonplaceholder.typicode.com/todos/1",
method: "GET",
- headers: [{ key: "Content-Type", value: "application/json" }]
+ headers: [{ key: "Content-Type", value: "application/json" }],
});
const result = await task.run();
@@ -62,18 +62,18 @@ describe("FetchTask", async () => {
expect(
// // @ts-expect-error
- () => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: 1 })
+ () => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: 1 }),
).toThrow();
expect(
new FetchTask("", {
url: "https://jsonplaceholder.typicode.com",
- method: "invalid"
- }).execute()
+ method: "invalid",
+ }).execute(),
).rejects.toThrow(/^Invalid method/);
expect(
- () => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: "GET" })
+ () => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: "GET" }),
).toBeDefined();
expect(() => new FetchTask("", { url: "", method: "Invalid" })).toThrow();
@@ -85,17 +85,17 @@ describe("FetchTask", async () => {
method: "{{ flow.output.method }}",
headers: [
{ key: "Content-{{ flow.output.headerKey }}", value: "application/json" },
- { key: "Authorization", value: "Bearer {{ flow.output.apiKey }}" }
+ { key: "Authorization", value: "Bearer {{ flow.output.apiKey }}" },
],
body: JSON.stringify({
- email: "{{ flow.output.email }}"
- })
+ email: "{{ flow.output.email }}",
+ }),
});
const inputs = {
headerKey: "Type",
apiKey: 123,
email: "what@else.com",
- method: "PATCH"
+ method: "PATCH",
};
const flow = new Flow("", [task]);
diff --git a/app/__test__/flows/SubWorkflowTask.spec.ts b/app/__test__/flows/SubWorkflowTask.spec.ts
index c05faf7..c52a0a2 100644
--- a/app/__test__/flows/SubWorkflowTask.spec.ts
+++ b/app/__test__/flows/SubWorkflowTask.spec.ts
@@ -4,16 +4,16 @@ import { Flow, LogTask, RenderTask, SubFlowTask } from "../../src/flows";
describe("SubFlowTask", async () => {
test("Simple Subflow", async () => {
const subTask = new RenderTask("render", {
- render: "subflow"
+ render: "subflow",
});
const subflow = new Flow("subflow", [subTask]);
const task = new LogTask("log");
const task2 = new SubFlowTask("sub", {
- flow: subflow
+ flow: subflow,
});
const task3 = new RenderTask("render2", {
- render: "Subflow output: {{ sub.output }}"
+ render: "Subflow output: {{ sub.output }}",
});
const flow = new Flow("test", [task, task2, task3], []);
@@ -30,7 +30,7 @@ describe("SubFlowTask", async () => {
test("Simple loop", async () => {
const subTask = new RenderTask("render", {
- render: "run {{ flow.output }}"
+ render: "run {{ flow.output }}",
});
const subflow = new Flow("subflow", [subTask]);
@@ -38,10 +38,10 @@ describe("SubFlowTask", async () => {
const task2 = new SubFlowTask("sub", {
flow: subflow,
loop: true,
- input: [1, 2, 3]
+ input: [1, 2, 3],
});
const task3 = new RenderTask("render2", {
- render: `Subflow output: {{ sub.output | join: ", " }}`
+ render: `Subflow output: {{ sub.output | join: ", " }}`,
});
const flow = new Flow("test", [task, task2, task3], []);
@@ -61,7 +61,7 @@ describe("SubFlowTask", async () => {
test("Simple loop from flow input", async () => {
const subTask = new RenderTask("render", {
- render: "run {{ flow.output }}"
+ render: "run {{ flow.output }}",
});
const subflow = new Flow("subflow", [subTask]);
@@ -70,10 +70,10 @@ describe("SubFlowTask", async () => {
const task2 = new SubFlowTask("sub", {
flow: subflow,
loop: true,
- input: "{{ flow.output | json }}"
+ input: "{{ flow.output | json }}",
});
const task3 = new RenderTask("render2", {
- render: `Subflow output: {{ sub.output | join: ", " }}`
+ render: `Subflow output: {{ sub.output | join: ", " }}`,
});
const flow = new Flow("test", [task, task2, task3], []);
diff --git a/app/__test__/flows/Task.spec.ts b/app/__test__/flows/Task.spec.ts
index 8519288..2016478 100644
--- a/app/__test__/flows/Task.spec.ts
+++ b/app/__test__/flows/Task.spec.ts
@@ -8,13 +8,13 @@ describe("Task", async () => {
const result = await Task.resolveParams(
Type.Object({ test: dynamic(Type.Number()) }),
{
- test: "{{ some.path }}"
+ test: "{{ some.path }}",
},
{
some: {
- path: 1
- }
- }
+ path: 1,
+ },
+ },
);
expect(result.test).toBe(1);
@@ -24,13 +24,13 @@ describe("Task", async () => {
const result = await Task.resolveParams(
Type.Object({ test: Type.String() }),
{
- test: "{{ some.path }}"
+ test: "{{ some.path }}",
},
{
some: {
- path: "1/1"
- }
- }
+ path: "1/1",
+ },
+ },
);
expect(result.test).toBe("1/1");
@@ -40,13 +40,13 @@ describe("Task", async () => {
const result = await Task.resolveParams(
Type.Object({ test: dynamic(Type.Object({ key: Type.String(), value: Type.String() })) }),
{
- test: { key: "path", value: "{{ some.path }}" }
+ test: { key: "path", value: "{{ some.path }}" },
},
{
some: {
- path: "1/1"
- }
- }
+ path: "1/1",
+ },
+ },
);
expect(result.test).toEqual({ key: "path", value: "1/1" });
@@ -55,17 +55,17 @@ describe("Task", async () => {
test("resolveParams: with json", async () => {
const result = await Task.resolveParams(
Type.Object({
- test: dynamic(Type.Object({ key: Type.String(), value: Type.String() }))
+ test: dynamic(Type.Object({ key: Type.String(), value: Type.String() })),
}),
{
- test: "{{ some | json }}"
+ test: "{{ some | json }}",
},
{
some: {
key: "path",
- value: "1/1"
- }
- }
+ value: "1/1",
+ },
+ },
);
expect(result.test).toEqual({ key: "path", value: "1/1" });
@@ -74,11 +74,11 @@ describe("Task", async () => {
test("resolveParams: with array", async () => {
const result = await Task.resolveParams(
Type.Object({
- test: dynamic(Type.Array(Type.String()))
+ test: dynamic(Type.Array(Type.String())),
}),
{
- test: '{{ "1,2,3" | split: "," | json }}'
- }
+ test: '{{ "1,2,3" | split: "," | json }}',
+ },
);
expect(result.test).toEqual(["1", "2", "3"]);
@@ -87,11 +87,11 @@ describe("Task", async () => {
test("resolveParams: boolean", async () => {
const result = await Task.resolveParams(
Type.Object({
- test: dynamic(Type.Boolean())
+ test: dynamic(Type.Boolean()),
}),
{
- test: "{{ true }}"
- }
+ test: "{{ true }}",
+ },
);
expect(result.test).toEqual(true);
@@ -100,11 +100,11 @@ describe("Task", async () => {
test("resolveParams: float", async () => {
const result = await Task.resolveParams(
Type.Object({
- test: dynamic(Type.Number(), Number.parseFloat)
+ test: dynamic(Type.Number(), Number.parseFloat),
}),
{
- test: "{{ 3.14 }}"
- }
+ test: "{{ 3.14 }}",
+ },
);
expect(result.test).toEqual(3.14);
diff --git a/app/__test__/flows/inc/fanout-condition.ts b/app/__test__/flows/inc/fanout-condition.ts
index c2ec3f5..c0ab97d 100644
--- a/app/__test__/flows/inc/fanout-condition.ts
+++ b/app/__test__/flows/inc/fanout-condition.ts
@@ -7,11 +7,11 @@ const first = getNamedTask(
//throw new Error("Error");
return {
inner: {
- result: 2
- }
+ result: 2,
+ },
};
},
- 1000
+ 1000,
);
const second = getNamedTask("second (if match)");
const third = getNamedTask("third (if error)");
diff --git a/app/__test__/flows/inc/helper.tsx b/app/__test__/flows/inc/helper.tsx
index 2893e3a..8e2ddb8 100644
--- a/app/__test__/flows/inc/helper.tsx
+++ b/app/__test__/flows/inc/helper.tsx
@@ -11,7 +11,7 @@ class ExecTask extends Task {
constructor(
name: string,
params: any,
- private fn: () => any
+ private fn: () => any,
) {
super(name, params);
}
@@ -54,8 +54,8 @@ export function getNamedTask(name: string, _func?: () => Promise, delay?: n
return new ExecTask(
name,
{
- delay
+ delay,
},
- func
+ func,
);
}
diff --git a/app/__test__/flows/inc/simple-fetch.ts b/app/__test__/flows/inc/simple-fetch.ts
index b5d21c3..7448e92 100644
--- a/app/__test__/flows/inc/simple-fetch.ts
+++ b/app/__test__/flows/inc/simple-fetch.ts
@@ -4,7 +4,7 @@ const first = new LogTask("First", { delay: 1000 });
const second = new LogTask("Second", { delay: 1000 });
const third = new LogTask("Long Third", { delay: 2500 });
const fourth = new FetchTask("Fetch Something", {
- url: "https://jsonplaceholder.typicode.com/todos/1"
+ url: "https://jsonplaceholder.typicode.com/todos/1",
});
const fifth = new LogTask("Task 4", { delay: 500 }); // without connection
diff --git a/app/__test__/flows/inputs.test.ts b/app/__test__/flows/inputs.test.ts
index 22d69f8..25a1c82 100644
--- a/app/__test__/flows/inputs.test.ts
+++ b/app/__test__/flows/inputs.test.ts
@@ -23,10 +23,10 @@ class OutputParamTask extends Task {
static override schema = Type.Object({
number: dynamic(
Type.Number({
- title: "Output number"
+ title: "Output number",
}),
- Number.parseInt
- )
+ Number.parseInt,
+ ),
});
async execute(inputs: InputsMap) {
@@ -75,7 +75,7 @@ describe("Flow task inputs", async () => {
test("output/input", async () => {
const task = new OutputParamTask("task1", { number: 111 });
const task2 = new OutputParamTask("task2", {
- number: "{{ task1.output }}"
+ number: "{{ task1.output }}",
});
const flow = new Flow("test", [task, task2]);
@@ -94,10 +94,10 @@ describe("Flow task inputs", async () => {
test("input from flow", async () => {
const task = new OutputParamTask("task1", {
- number: "{{flow.output.someFancyParam}}"
+ number: "{{flow.output.someFancyParam}}",
});
const task2 = new OutputParamTask("task2", {
- number: "{{task1.output}}"
+ number: "{{task1.output}}",
});
const flow = new Flow("test", [task, task2]);
@@ -126,7 +126,7 @@ describe("Flow task inputs", async () => {
const emgr = new EventManager({ EventTriggerClass });
const task = new OutputParamTask("event", {
- number: "{{flow.output.number}}"
+ number: "{{flow.output.number}}",
});
const flow = new Flow(
"test",
@@ -134,8 +134,8 @@ describe("Flow task inputs", async () => {
[],
new EventTrigger({
event: "test-event",
- mode: "sync"
- })
+ mode: "sync",
+ }),
);
flow.setRespondingTask(task);
flow.trigger.register(flow, emgr);
@@ -155,8 +155,8 @@ describe("Flow task inputs", async () => {
new HttpTrigger({
path: "/test",
method: "GET",
- mode: "sync"
- })
+ mode: "sync",
+ }),
);
flow.setRespondingTask(task);
diff --git a/app/__test__/flows/render.tsx b/app/__test__/flows/render.tsx
index 8080b5c..61867e6 100644
--- a/app/__test__/flows/render.tsx
+++ b/app/__test__/flows/render.tsx
@@ -10,7 +10,7 @@ const flows = {
back,
fanout,
parallel,
- simpleFetch
+ simpleFetch,
};
const arg = process.argv[2];
@@ -32,7 +32,7 @@ const colors = [
"#F78F1E", // Saffron
"#BD10E0", // Vivid Purple
"#50E3C2", // Turquoise
- "#9013FE" // Grape
+ "#9013FE", // Grape
];
const colorsCache: Record = {};
@@ -82,7 +82,7 @@ function TerminalFlow({ flow }: { flow: Flow }) {
}
return t;
- })
+ }),
);
}
});
@@ -92,7 +92,7 @@ function TerminalFlow({ flow }: { flow: Flow }) {
console.log("done", response ? response : "(no response)");
console.log(
"Executed tasks:",
- execution.logs.map((l) => l.task.name)
+ execution.logs.map((l) => l.task.name),
);
console.log("Executed count:", execution.logs.length);
});
diff --git a/app/__test__/flows/trigger.test.ts b/app/__test__/flows/trigger.test.ts
index 1fec803..e85f13e 100644
--- a/app/__test__/flows/trigger.test.ts
+++ b/app/__test__/flows/trigger.test.ts
@@ -11,7 +11,7 @@ class ExecTask extends Task {
constructor(
name: string,
params: any,
- private fn: () => any
+ private fn: () => any,
) {
super(name, params);
}
@@ -60,7 +60,7 @@ describe("Flow trigger", async () => {
"test",
[task],
[],
- new EventTrigger({ event: "test-event", mode: "sync" })
+ new EventTrigger({ event: "test-event", mode: "sync" }),
);
flow.trigger.register(flow, emgr);
@@ -107,8 +107,8 @@ describe("Flow trigger", async () => {
new HttpTrigger({
path: "/test",
method: "GET",
- mode: "sync"
- })
+ mode: "sync",
+ }),
);
const hono = new Hono();
@@ -123,7 +123,7 @@ describe("Flow trigger", async () => {
test("http trigger with response", async () => {
const task = ExecTask.create("http", () => ({
- called: true
+ called: true,
}));
const flow = new Flow(
"test",
@@ -132,8 +132,8 @@ describe("Flow trigger", async () => {
new HttpTrigger({
path: "/test",
method: "GET",
- mode: "sync"
- })
+ mode: "sync",
+ }),
);
flow.setRespondingTask(task);
diff --git a/app/__test__/flows/workflow-basic.test.ts b/app/__test__/flows/workflow-basic.test.ts
index 6a0f1d6..56579f5 100644
--- a/app/__test__/flows/workflow-basic.test.ts
+++ b/app/__test__/flows/workflow-basic.test.ts
@@ -11,13 +11,13 @@ class ExecTask extends Task {
type = "exec";
static override schema = Type.Object({
- delay: Type.Number({ default: 10 })
+ delay: Type.Number({ default: 10 }),
});
constructor(
name: string,
params: Static,
- private func: () => Promise
+ private func: () => Promise,
) {
super(name, params);
}
@@ -36,12 +36,12 @@ function getTask(num: number = 0, delay: number = 5) {
return new ExecTask(
`Task ${num}`,
{
- delay
+ delay,
},
async () => {
//console.log(`[DONE] Task: ${num}`);
return true;
- }
+ },
);
//return new LogTask(`Log ${num}`, { delay });
}
@@ -56,9 +56,9 @@ function getNamedTask(name: string, _func?: () => Promise, delay?: number)
return new ExecTask(
name,
{
- delay: delay ?? 0
+ delay: delay ?? 0,
},
- func
+ func,
);
}
@@ -228,7 +228,7 @@ describe("Flow tests", async () => {
back
.task(third)
.getOutTasks()
- .map((t) => t.name)
+ .map((t) => t.name),
).toEqual(["second", "fourth"]);
const execution = back.createExecution();
@@ -263,7 +263,7 @@ describe("Flow tests", async () => {
back
.task(third)
.getOutTasks()
- .map((t) => t.name)
+ .map((t) => t.name),
).toEqual(["second", "fourth"]);
const execution = back.createExecution();
@@ -324,8 +324,8 @@ describe("Flow tests", async () => {
const first = getNamedTask("first", async () => {
return {
inner: {
- result: 2
- }
+ result: 2,
+ },
};
});
const second = getNamedTask("second");
@@ -361,7 +361,7 @@ describe("Flow tests", async () => {
"[event]",
event.isStart() ? "start" : "end",
event.task().name,
- event.isStart() ? undefined : event.succeeded()
+ event.isStart() ? undefined : event.succeeded(),
);
}
});
@@ -389,7 +389,7 @@ describe("Flow tests", async () => {
const second = new LogTask("Task 1");
const third = new LogTask("Task 2", { delay: 50 });
const fourth = new FetchTask("Fetch Something", {
- url: "https://jsonplaceholder.typicode.com/todos/1"
+ url: "https://jsonplaceholder.typicode.com/todos/1",
});
const fifth = new LogTask("Task 4"); // without connection
@@ -405,7 +405,7 @@ describe("Flow tests", async () => {
// @todo: fix
const deserialized = Flow.fromObject("", original, {
fetch: { cls: FetchTask },
- log: { cls: LogTask }
+ log: { cls: LogTask },
} as any);
const diffdeep = getObjectDiff(original, deserialized.toJSON());
@@ -414,7 +414,7 @@ describe("Flow tests", async () => {
expect(flow.startTask.name).toEqual(deserialized.startTask.name);
expect(flow.respondingTask?.name).toEqual(
// @ts-ignore
- deserialized.respondingTask?.name
+ deserialized.respondingTask?.name,
);
//console.log("--- creating original sequence");
diff --git a/app/__test__/helper.ts b/app/__test__/helper.ts
index 8cafff2..405e46f 100644
--- a/app/__test__/helper.ts
+++ b/app/__test__/helper.ts
@@ -4,6 +4,9 @@ import Database from "libsql";
import { format as sqlFormat } from "sql-formatter";
import { type Connection, EntityManager, SqliteLocalConnection } from "../src/data";
import type { em as protoEm } from "../src/data/prototype";
+import { writeFile } from "node:fs/promises";
+import { join } from "node:path";
+import { slugify } from "core/utils/strings";
export function getDummyDatabase(memory: boolean = true): {
dummyDb: SqliteDatabase;
@@ -17,7 +20,7 @@ export function getDummyDatabase(memory: boolean = true): {
afterAllCleanup: async () => {
if (!memory) await unlink(DB_NAME);
return true;
- }
+ },
};
}
@@ -27,7 +30,7 @@ export function getDummyConnection(memory: boolean = true) {
return {
dummyConnection,
- afterAllCleanup
+ afterAllCleanup,
};
}
@@ -39,7 +42,7 @@ type ConsoleSeverity = "log" | "warn" | "error";
const _oldConsoles = {
log: console.log,
warn: console.warn,
- error: console.error
+ error: console.error,
};
export function disableConsoleLog(severities: ConsoleSeverity[] = ["log", "warn"]) {
@@ -71,3 +74,46 @@ export function schemaToEm(s: ReturnType, conn?: Connection): En
export const assetsPath = `${import.meta.dir}/_assets`;
export const assetsTmpPath = `${import.meta.dir}/_assets/tmp`;
+
+export async function enableFetchLogging() {
+ const originalFetch = global.fetch;
+
+ global.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
+ const response = await originalFetch(input, init);
+ const url = input instanceof URL || typeof input === "string" ? input : input.url;
+
+ // Only clone if it's a supported content type
+ const contentType = response.headers.get("content-type") || "";
+ const isSupported =
+ contentType.includes("json") ||
+ contentType.includes("text") ||
+ contentType.includes("xml");
+
+ if (isSupported) {
+ const clonedResponse = response.clone();
+ let extension = "txt";
+ let body: string;
+
+ if (contentType.includes("json")) {
+ body = JSON.stringify(await clonedResponse.json(), null, 2);
+ extension = "json";
+ } else if (contentType.includes("xml")) {
+ body = await clonedResponse.text();
+ extension = "xml";
+ } else {
+ body = await clonedResponse.text();
+ }
+
+ const fileName = `${new Date().getTime()}_${init?.method ?? "GET"}_${slugify(String(url))}.${extension}`;
+ const filePath = join(assetsTmpPath, fileName);
+
+ await writeFile(filePath, body);
+ }
+
+ return response;
+ };
+
+ return () => {
+ global.fetch = originalFetch;
+ };
+}
diff --git a/app/__test__/integration/auth.integration.test.ts b/app/__test__/integration/auth.integration.test.ts
index 6f2466c..9b2bb51 100644
--- a/app/__test__/integration/auth.integration.test.ts
+++ b/app/__test__/integration/auth.integration.test.ts
@@ -16,25 +16,25 @@ const roles = {
"system.schema.read",
"system.access.api",
"system.config.read",
- "data.entity.read"
+ "data.entity.read",
],
- is_default: true
+ is_default: true,
},
admin: {
is_default: true,
- implicit_allow: true
- }
+ implicit_allow: true,
+ },
},
strict: {
guest: {
permissions: ["system.access.api", "system.config.read", "data.entity.read"],
- is_default: true
+ is_default: true,
},
admin: {
is_default: true,
- implicit_allow: true
- }
- }
+ implicit_allow: true,
+ },
+ },
};
const configs = {
auth: {
@@ -42,31 +42,31 @@ const configs = {
entity_name: "users",
jwt: {
secret: secureRandomString(20),
- issuer: randomString(10)
+ issuer: randomString(10),
},
roles: roles.strict,
guard: {
- enabled: true
- }
+ enabled: true,
+ },
},
users: {
normal: {
email: "normal@bknd.io",
- password: "12345678"
+ password: "12345678",
},
admin: {
email: "admin@bknd.io",
password: "12345678",
- role: "admin"
- }
- }
+ role: "admin",
+ },
+ },
};
function createAuthApp() {
const app = createApp({
initialConfig: {
- auth: configs.auth
- }
+ auth: configs.auth,
+ },
});
app.emgr.onEvent(
@@ -75,7 +75,7 @@ function createAuthApp() {
await app.createUser(configs.users.normal);
await app.createUser(configs.users.admin);
},
- "sync"
+ "sync",
);
return app;
@@ -94,14 +94,14 @@ const fns = (app: App, mode?: Mode) =
if (mode === "cookie") {
return {
cookie: `auth=${token};`,
- ...additional
+ ...additional,
};
}
return {
Authorization: token ? `Bearer ${token}` : "",
"Content-Type": "application/json",
- ...additional
+ ...additional,
};
}
function body(obj?: Record) {
@@ -118,12 +118,12 @@ const fns = (app: App, mode?: Mode) =
return {
login: async (
- user: any
+ user: any,
): Promise<{ res: Response; data: Mode extends "token" ? AuthResponse : string }> => {
const res = (await app.server.request("/api/auth/password/login", {
method: "POST",
headers: headers(),
- body: body(user)
+ body: body(user),
})) as Response;
const data = mode === "cookie" ? getCookie(res, "auth") : await res.json();
@@ -133,10 +133,10 @@ const fns = (app: App, mode?: Mode) =
me: async (token?: string): Promise> => {
const res = (await app.server.request("/api/auth/me", {
method: "GET",
- headers: headers(token)
+ headers: headers(token),
})) as Response;
return await res.json();
- }
+ },
};
};
@@ -219,7 +219,7 @@ describe("integration auth", () => {
app.server.get("/get", auth(), async (c) => {
return c.json({
- user: c.get("auth").user ?? null
+ user: c.get("auth").user ?? null,
});
});
app.server.get("/wait", auth(), async (c) => {
@@ -232,7 +232,7 @@ describe("integration auth", () => {
expect(me.user.email).toBe(configs.users.normal.email);
app.server.request("/wait", {
- headers: { Authorization: `Bearer ${data.token}` }
+ headers: { Authorization: `Bearer ${data.token}` },
});
{
diff --git a/app/__test__/integration/config.integration.test.ts b/app/__test__/integration/config.integration.test.ts
index 6eef035..f370e69 100644
--- a/app/__test__/integration/config.integration.test.ts
+++ b/app/__test__/integration/config.integration.test.ts
@@ -8,7 +8,7 @@ describe("integration config", () => {
await app.build();
const api = new Api({
host: "http://localhost",
- fetcher: app.server.request as typeof fetch
+ fetcher: app.server.request as typeof fetch,
});
// create entity
@@ -16,7 +16,7 @@ describe("integration config", () => {
name: "posts",
config: { sort_field: "id", sort_dir: "asc" },
fields: { id: { type: "primary", name: "id" }, asdf: { type: "text" } },
- type: "regular"
+ type: "regular",
});
expect(app.em.entities.map((e) => e.name)).toContain("posts");
diff --git a/app/__test__/media/MediaController.spec.ts b/app/__test__/media/MediaController.spec.ts
index 49b0900..3584317 100644
--- a/app/__test__/media/MediaController.spec.ts
+++ b/app/__test__/media/MediaController.spec.ts
@@ -2,9 +2,9 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { createApp, registries } from "../../src";
-import { StorageLocalAdapter } from "../../src/adapter/node";
import { mergeObject, randomString } from "../../src/core/utils";
import type { TAppMediaConfig } from "../../src/media/media-schema";
+import { StorageLocalAdapter } from "../../src/media/storage/adapters/StorageLocalAdapter";
import { assetsPath, assetsTmpPath, disableConsoleLog, enableConsoleLog } from "../helper";
beforeAll(() => {
@@ -22,13 +22,13 @@ async function makeApp(mediaOverride: Partial = {}) {
adapter: {
type: "local",
config: {
- path: assetsTmpPath
- }
- }
+ path: assetsTmpPath,
+ },
+ },
},
- mediaOverride
- )
- }
+ mediaOverride,
+ ),
+ },
});
await app.build();
@@ -43,16 +43,17 @@ beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
describe("MediaController", () => {
- test("accepts direct", async () => {
+ test.only("accepts direct", async () => {
const app = await makeApp();
const file = Bun.file(path);
const name = makeName("png");
const res = await app.server.request("/api/media/upload/" + name, {
method: "POST",
- body: file
+ body: file,
});
const result = (await res.json()) as any;
+ console.log(result);
expect(result.name).toBe(name);
const destFile = Bun.file(assetsTmpPath + "/" + name);
@@ -70,7 +71,7 @@ describe("MediaController", () => {
const res = await app.server.request("/api/media/upload/" + name, {
method: "POST",
- body: form
+ body: form,
});
const result = (await res.json()) as any;
expect(result.name).toBe(name);
@@ -87,7 +88,7 @@ describe("MediaController", () => {
const name = makeName("png");
const res = await app.server.request("/api/media/upload/" + name, {
method: "POST",
- body: file
+ body: file,
});
expect(res.status).toBe(413);
diff --git a/app/__test__/media/Storage.spec.ts b/app/__test__/media/Storage.spec.ts
index 69d1f0e..f493606 100644
--- a/app/__test__/media/Storage.spec.ts
+++ b/app/__test__/media/Storage.spec.ts
@@ -60,7 +60,7 @@ describe("Storage", async () => {
test("uploads a file", async () => {
const {
- meta: { type, size }
+ meta: { type, size },
} = await storage.uploadFile("hello", "world.txt");
expect({ type, size }).toEqual({ type: "text/plain", size: 0 });
});
diff --git a/app/__test__/media/StorageR2Adapter.native-spec.ts b/app/__test__/media/StorageR2Adapter.native-spec.ts
index 8f33ca6..64c7a9f 100644
--- a/app/__test__/media/StorageR2Adapter.native-spec.ts
+++ b/app/__test__/media/StorageR2Adapter.native-spec.ts
@@ -14,7 +14,7 @@ test("what", async () => {
const mf = new Miniflare({
modules: true,
script: "export default { async fetch() { return new Response(null); } }",
- r2Buckets: ["BUCKET"]
+ r2Buckets: ["BUCKET"],
});
const bucket = await mf.getR2Bucket("BUCKET");
diff --git a/app/__test__/media/adapters/StorageCloudinaryAdapter.spec.ts b/app/__test__/media/adapters/StorageCloudinaryAdapter.spec.ts
index e2457b2..9cac2e4 100644
--- a/app/__test__/media/adapters/StorageCloudinaryAdapter.spec.ts
+++ b/app/__test__/media/adapters/StorageCloudinaryAdapter.spec.ts
@@ -8,17 +8,19 @@ const {
CLOUDINARY_CLOUD_NAME,
CLOUDINARY_API_KEY,
CLOUDINARY_API_SECRET,
- CLOUDINARY_UPLOAD_PRESET
+ CLOUDINARY_UPLOAD_PRESET,
} = dotenvOutput.parsed!;
const ALL_TESTS = !!process.env.ALL_TESTS;
describe.skipIf(ALL_TESTS)("StorageCloudinaryAdapter", () => {
+ if (ALL_TESTS) return;
+
const adapter = new StorageCloudinaryAdapter({
cloud_name: CLOUDINARY_CLOUD_NAME as string,
api_key: CLOUDINARY_API_KEY as string,
api_secret: CLOUDINARY_API_SECRET as string,
- upload_preset: CLOUDINARY_UPLOAD_PRESET as string
+ upload_preset: CLOUDINARY_UPLOAD_PRESET as string,
});
const file = Bun.file(`${import.meta.dir}/icon.png`);
diff --git a/app/__test__/media/adapters/StorageLocalAdapter.spec.ts b/app/__test__/media/adapters/StorageLocalAdapter.spec.ts
index 2240aeb..b23f84d 100644
--- a/app/__test__/media/adapters/StorageLocalAdapter.spec.ts
+++ b/app/__test__/media/adapters/StorageLocalAdapter.spec.ts
@@ -1,13 +1,14 @@
import { describe, expect, test } from "bun:test";
import { randomString } from "../../../src/core/utils";
import { StorageLocalAdapter } from "../../../src/media/storage/adapters/StorageLocalAdapter";
+import { assetsPath, assetsTmpPath } from "../../helper";
describe("StorageLocalAdapter", () => {
const adapter = new StorageLocalAdapter({
- path: `${import.meta.dir}/local`
+ path: assetsTmpPath,
});
- const file = Bun.file(`${import.meta.dir}/icon.png`);
+ const file = Bun.file(`${assetsPath}/image.png`);
const _filename = randomString(10);
const filename = `${_filename}.png`;
@@ -35,7 +36,7 @@ describe("StorageLocalAdapter", () => {
test("gets object meta", async () => {
expect(await adapter.getObjectMeta(filename)).toEqual({
type: file.type, // image/png
- size: file.size
+ size: file.size,
});
});
diff --git a/app/__test__/media/adapters/StorageS3Adapter.spec.ts b/app/__test__/media/adapters/StorageS3Adapter.spec.ts
index 7ea77b1..7b4a0a4 100644
--- a/app/__test__/media/adapters/StorageS3Adapter.spec.ts
+++ b/app/__test__/media/adapters/StorageS3Adapter.spec.ts
@@ -1,34 +1,47 @@
-import { describe, expect, test } from "bun:test";
+import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { randomString } from "../../../src/core/utils";
import { StorageS3Adapter } from "../../../src/media";
import { config } from "dotenv";
+//import { enableFetchLogging } from "../../helper";
const dotenvOutput = config({ path: `${import.meta.dir}/../../../.env` });
const { R2_ACCESS_KEY, R2_SECRET_ACCESS_KEY, R2_URL, AWS_ACCESS_KEY, AWS_SECRET_KEY, AWS_S3_URL } =
dotenvOutput.parsed!;
// @todo: mock r2/s3 responses for faster tests
const ALL_TESTS = !!process.env.ALL_TESTS;
+console.log("ALL_TESTS?", ALL_TESTS);
+
+/*
+// @todo: preparation to mock s3 calls + replace fast-xml-parser
+let cleanup: () => void;
+beforeAll(async () => {
+ cleanup = await enableFetchLogging();
+});
+afterAll(() => {
+ cleanup();
+}); */
describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => {
- console.log("ALL_TESTS", process.env.ALL_TESTS);
+ if (ALL_TESTS) return;
+
const versions = [
[
"r2",
new StorageS3Adapter({
access_key: R2_ACCESS_KEY as string,
secret_access_key: R2_SECRET_ACCESS_KEY as string,
- url: R2_URL as string
- })
+ url: R2_URL as string,
+ }),
],
[
"s3",
new StorageS3Adapter({
access_key: AWS_ACCESS_KEY as string,
secret_access_key: AWS_SECRET_KEY as string,
- url: AWS_S3_URL as string
- })
- ]
+ url: AWS_S3_URL as string,
+ }),
+ ],
] as const;
const _conf = {
@@ -39,8 +52,8 @@ describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => {
"objectExists",
"getObject",
"deleteObject",
- "getObjectMeta"
- ]
+ "getObjectMeta",
+ ],
};
const file = Bun.file(`${import.meta.dir}/icon.png`);
@@ -55,7 +68,7 @@ describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => {
// @todo: add mocked fetch for faster tests
describe.each(versions)("StorageS3Adapter for %s", async (name, adapter) => {
- if (!_conf.adapters.includes(name)) {
+ if (!_conf.adapters.includes(name) || ALL_TESTS) {
console.log("Skipping", name);
return;
}
@@ -64,7 +77,7 @@ describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => {
test.skipIf(disabled("putObject"))("puts an object", async () => {
objects = (await adapter.listObjects()).length;
- expect(await adapter.putObject(filename, file)).toBeString();
+ expect(await adapter.putObject(filename, file as any)).toBeString();
});
test.skipIf(disabled("listObjects"))("lists objects", async () => {
@@ -84,7 +97,7 @@ describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => {
test.skipIf(disabled("getObjectMeta"))("gets object meta", async () => {
expect(await adapter.getObjectMeta(filename)).toEqual({
type: file.type, // image/png
- size: file.size
+ size: file.size,
});
});
diff --git a/app/__test__/media/mime-types.spec.ts b/app/__test__/media/mime-types.spec.ts
index 55469ff..6c51fab 100644
--- a/app/__test__/media/mime-types.spec.ts
+++ b/app/__test__/media/mime-types.spec.ts
@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test";
import * as large from "../../src/media/storage/mime-types";
import * as tiny from "../../src/media/storage/mime-types-tiny";
+import { getRandomizedFilename } from "../../src/media/utils";
describe("media/mime-types", () => {
test("tiny resolves", () => {
@@ -27,7 +28,7 @@ describe("media/mime-types", () => {
exts,
ext,
expected: ex,
- actual: large.guessMimeType("." + ext)
+ actual: large.guessMimeType("." + ext),
});
throw new Error(`Failed for ${ext}`);
}
@@ -36,19 +37,62 @@ describe("media/mime-types", () => {
});
test("isMimeType", () => {
- expect(tiny.isMimeType("image/jpeg")).toBe(true);
- expect(tiny.isMimeType("image/jpeg", ["image/png"])).toBe(true);
- expect(tiny.isMimeType("image/png", ["image/png"])).toBe(false);
- expect(tiny.isMimeType("image/png")).toBe(true);
- expect(tiny.isMimeType("whatever")).toBe(false);
- expect(tiny.isMimeType("text/tab-separated-values")).toBe(true);
+ const tests = [
+ ["image/avif", true],
+ ["image/AVIF", true],
+ ["image/jpeg", true],
+ ["image/jpeg", true, ["image/png"]],
+ ["image/png", false, ["image/png"]],
+ ["image/png", true],
+ ["image/heif", true],
+ ["image/heic", true],
+ ["image/gif", true],
+ ["whatever", false],
+ ["text/tab-separated-values", true],
+ ["application/zip", true],
+ ];
+
+ for (const [mime, expected, exclude] of tests) {
+ expect(
+ tiny.isMimeType(mime, exclude as any),
+ `isMimeType(): ${mime} should be ${expected}`,
+ ).toBe(expected as any);
+ }
});
test("extension", () => {
- expect(tiny.extension("image/png")).toBe("png");
- expect(tiny.extension("image/jpeg")).toBe("jpeg");
- expect(tiny.extension("application/zip")).toBe("zip");
- expect(tiny.extension("text/tab-separated-values")).toBe("tsv");
- expect(tiny.extension("application/zip")).toBe("zip");
+ const tests = [
+ ["image/avif", "avif"],
+ ["image/png", "png"],
+ ["image/PNG", "png"],
+ ["image/jpeg", "jpeg"],
+ ["application/zip", "zip"],
+ ["text/tab-separated-values", "tsv"],
+ ["application/zip", "zip"],
+ ];
+
+ for (const [mime, ext] of tests) {
+ expect(tiny.extension(mime), `extension(): ${mime} should be ${ext}`).toBe(ext);
+ }
+ });
+
+ test("getRandomizedFilename", () => {
+ const tests = [
+ ["file.txt", "txt"],
+ ["file.TXT", "txt"],
+ ["image.jpg", "jpg"],
+ ["image.avif", "avif"],
+ ["image.heic", "heic"],
+ ["image.jpeg", "jpeg"],
+ ["-473Wx593H-466453554-black-MODEL.jpg", "jpg"],
+ ["-473Wx593H-466453554-black-MODEL.avif", "avif"],
+ ];
+
+ for (const [filename, ext] of tests) {
+ expect(
+ getRandomizedFilename(filename).split(".").pop(),
+ `getRandomizedFilename(): ${filename} should end with ${ext}`,
+ ).toBe(ext);
+ }
});
});
diff --git a/app/__test__/modules/AppAuth.spec.ts b/app/__test__/modules/AppAuth.spec.ts
index f0ecc86..ca861d5 100644
--- a/app/__test__/modules/AppAuth.spec.ts
+++ b/app/__test__/modules/AppAuth.spec.ts
@@ -43,10 +43,10 @@ describe("AppAuth", () => {
{
enabled: true,
jwt: {
- secret: "123456"
- }
+ secret: "123456",
+ },
},
- ctx
+ ctx,
);
await auth.build();
@@ -63,12 +63,12 @@ describe("AppAuth", () => {
const res = await app.request("/password/register", {
method: "POST",
headers: {
- "Content-Type": "application/json"
+ "Content-Type": "application/json",
},
body: JSON.stringify({
email: "some@body.com",
- password: "123456"
- })
+ password: "123456",
+ }),
});
enableConsoleLog();
expect(res.status).toBe(200);
@@ -85,10 +85,10 @@ describe("AppAuth", () => {
auth: {
enabled: true,
jwt: {
- secret: "123456"
- }
- }
- }
+ secret: "123456",
+ },
+ },
+ },
});
await app.build();
@@ -109,14 +109,14 @@ describe("AppAuth", () => {
initialConfig: {
auth: {
entity_name: "users",
- enabled: true
+ enabled: true,
},
data: em({
users: entity("users", {
- additional: text()
- })
- }).toJSON()
- }
+ additional: text(),
+ }),
+ }).toJSON(),
+ },
});
await app.build();
@@ -132,21 +132,21 @@ describe("AppAuth", () => {
const app = createApp({
initialConfig: {
auth: {
- enabled: true
+ enabled: true,
},
data: em({
users: entity("users", {
strategy: text({
fillable: true,
- hidden: false
+ hidden: false,
}),
strategy_value: text({
fillable: true,
- hidden: false
- })
- })
- }).toJSON()
- }
+ hidden: false,
+ }),
+ }),
+ }).toJSON(),
+ },
});
await app.build();
@@ -157,7 +157,7 @@ describe("AppAuth", () => {
const authField = make(name, _authFieldProto as any);
const field = users.field(name)!;
for (const prop of props) {
- expect(field.config[prop]).toBe(authField.config[prop]);
+ expect(field.config[prop]).toEqual(authField.config[prop]);
}
}
});
diff --git a/app/__test__/modules/AppMedia.spec.ts b/app/__test__/modules/AppMedia.spec.ts
index b5ce17f..1423fd6 100644
--- a/app/__test__/modules/AppMedia.spec.ts
+++ b/app/__test__/modules/AppMedia.spec.ts
@@ -19,16 +19,16 @@ describe("AppMedia", () => {
adapter: {
type: "local",
config: {
- path: "./"
- }
- }
+ path: "./",
+ },
+ },
},
data: em({
media: entity("media", {
- additional: text()
- })
- }).toJSON()
- }
+ additional: text(),
+ }),
+ }).toJSON(),
+ },
});
await app.build();
@@ -49,7 +49,7 @@ describe("AppMedia", () => {
"modified_at",
"reference",
"entity_id",
- "metadata"
+ "metadata",
]);
});
});
diff --git a/app/__test__/modules/Module.spec.ts b/app/__test__/modules/Module.spec.ts
index 5c20ca5..bcb6aa8 100644
--- a/app/__test__/modules/Module.spec.ts
+++ b/app/__test__/modules/Module.spec.ts
@@ -47,7 +47,7 @@ describe("Module", async () => {
prt = {
ensureEntity: this.ensureEntity.bind(this),
ensureIndex: this.ensureIndex.bind(this),
- ensureSchema: this.ensureSchema.bind(this)
+ ensureSchema: this.ensureSchema.bind(this),
};
get em() {
@@ -60,7 +60,7 @@ describe("Module", async () => {
Object.values(_em.entities),
new DummyConnection(),
_em.relations,
- _em.indices
+ _em.indices,
);
return new M({} as any, { em, flags: Module.ctx_flags } as any);
}
@@ -69,14 +69,14 @@ describe("Module", async () => {
entities: _em.entities.map((e) => ({
name: e.name,
fields: e.fields.map((f) => f.name),
- type: e.type
+ type: e.type,
})),
indices: _em.indices.map((i) => ({
name: i.name,
entity: i.entity.name,
fields: i.fields.map((f) => f.name),
- unique: i.unique
- }))
+ unique: i.unique,
+ })),
};
}
@@ -88,15 +88,15 @@ describe("Module", async () => {
expect(flat(make(initial).em)).toEqual({
entities: [],
- indices: []
+ indices: [],
});
});
test("init", () => {
const initial = em({
users: entity("u", {
- name: text()
- })
+ name: text(),
+ }),
});
const m = make(initial);
@@ -107,18 +107,18 @@ describe("Module", async () => {
{
name: "u",
fields: ["id", "name"],
- type: "regular"
- }
+ type: "regular",
+ },
],
- indices: []
+ indices: [],
});
});
test("ensure entity", () => {
const initial = em({
users: entity("u", {
- name: text()
- })
+ name: text(),
+ }),
});
const m = make(initial);
@@ -127,17 +127,17 @@ describe("Module", async () => {
{
name: "u",
fields: ["id", "name"],
- type: "regular"
- }
+ type: "regular",
+ },
],
- indices: []
+ indices: [],
});
// this should add a new entity
m.prt.ensureEntity(
entity("p", {
- title: text()
- })
+ title: text(),
+ }),
);
// this should only add the field "important"
@@ -145,11 +145,11 @@ describe("Module", async () => {
entity(
"u",
{
- important: text()
+ important: text(),
},
undefined,
- "system"
- )
+ "system",
+ ),
);
expect(m.ctx.flags.sync_required).toBe(true);
@@ -159,22 +159,22 @@ describe("Module", async () => {
name: "u",
fields: ["id", "name", "important"],
// ensured type must be present
- type: "system"
+ type: "system",
},
{
name: "p",
fields: ["id", "title"],
- type: "regular"
- }
+ type: "regular",
+ },
],
- indices: []
+ indices: [],
});
});
test("ensure index", () => {
const users = entity("u", {
name: text(),
- title: text()
+ title: text(),
});
const initial = em({ users }, ({ index }, { users }) => {
index(users).on(["title"]);
@@ -189,23 +189,23 @@ describe("Module", async () => {
{
name: "u",
fields: ["id", "name", "title"],
- type: "regular"
- }
+ type: "regular",
+ },
],
indices: [
{
name: "idx_u_title",
entity: "u",
fields: ["title"],
- unique: false
+ unique: false,
},
{
name: "idx_u_name",
entity: "u",
fields: ["name"],
- unique: false
- }
- ]
+ unique: false,
+ },
+ ],
});
});
});
diff --git a/app/__test__/modules/ModuleManager.spec.ts b/app/__test__/modules/ModuleManager.spec.ts
index 64e3cde..c556795 100644
--- a/app/__test__/modules/ModuleManager.spec.ts
+++ b/app/__test__/modules/ModuleManager.spec.ts
@@ -39,10 +39,10 @@ describe("ModuleManager", async () => {
basepath: "/api/data2",
entities: {
test: entity("test", {
- content: text()
- }).toJSON()
- }
- }
+ content: text(),
+ }).toJSON(),
+ },
+ },
});
//const { version, ...json } = mm.toJSON() as any;
@@ -69,10 +69,10 @@ describe("ModuleManager", async () => {
basepath: "/api/data2",
entities: {
test: entity("test", {
- content: text()
- }).toJSON()
- }
- }
+ content: text(),
+ }).toJSON(),
+ },
+ },
};
//const { version, ...json } = mm.toJSON() as any;
@@ -105,7 +105,7 @@ describe("ModuleManager", async () => {
const c2 = getDummyConnection();
const db = c2.dummyConnection.kysely;
const mm2 = new ModuleManager(c2.dummyConnection, {
- initial: { version: version - 1, ...json }
+ initial: { version: version - 1, ...json },
});
await mm2.syncConfigTable();
@@ -129,7 +129,7 @@ describe("ModuleManager", async () => {
const db = c2.dummyConnection.kysely;
const mm2 = new ModuleManager(c2.dummyConnection, {
- initial: { version: version - 1, ...json }
+ initial: { version: version - 1, ...json },
});
await mm2.syncConfigTable();
await db
@@ -157,8 +157,8 @@ describe("ModuleManager", async () => {
...json,
data: {
...json.data,
- basepath: "/api/data2"
- }
+ basepath: "/api/data2",
+ },
};
await db
.insertInto(TABLE_NAME)
@@ -190,9 +190,9 @@ describe("ModuleManager", async () => {
...configs.server,
admin: {
...configs.server.admin,
- color_scheme: "dark"
- }
- }
+ color_scheme: "dark",
+ },
+ },
});
});
@@ -201,11 +201,11 @@ describe("ModuleManager", async () => {
const partial = {
auth: {
- enabled: true
- }
+ enabled: true,
+ },
};
const mm = new ModuleManager(dummyConnection, {
- initial: partial
+ initial: partial,
});
await mm.build();
@@ -227,9 +227,9 @@ describe("ModuleManager", async () => {
const mm2 = new ModuleManager(c2.dummyConnection, {
initial: {
auth: {
- basepath: "/shouldnt/take/this"
- }
- }
+ basepath: "/shouldnt/take/this",
+ },
+ },
});
await mm2.syncConfigTable();
const payload = {
@@ -237,15 +237,15 @@ describe("ModuleManager", async () => {
auth: {
...json.auth,
enabled: true,
- basepath: "/api/auth2"
- }
+ basepath: "/api/auth2",
+ },
};
await db
.insertInto(TABLE_NAME)
.values({
type: "config",
json: JSON.stringify(payload),
- version: CURRENT_VERSION
+ version: CURRENT_VERSION,
})
.execute();
await mm2.build();
@@ -256,7 +256,7 @@ describe("ModuleManager", async () => {
describe("revert", async () => {
const failingModuleSchema = Type.Object({
- value: Type.Optional(Type.Number())
+ value: Type.Optional(Type.Number()),
});
class FailingModule extends Module {
getSchema() {
@@ -301,8 +301,8 @@ describe("ModuleManager", async () => {
const mm = new TestModuleManager(dummyConnection, {
initial: {
// @ts-ignore
- failing: { value: 2 }
- }
+ failing: { value: 2 },
+ },
});
await mm.build();
expect(mm.configs()["failing"].value).toBe(2);
@@ -313,8 +313,8 @@ describe("ModuleManager", async () => {
const mm = new TestModuleManager(dummyConnection, {
initial: {
// @ts-ignore
- failing: { value: -1 }
- }
+ failing: { value: -1 },
+ },
});
expect(mm.build()).rejects.toThrow(/value must be positive/);
expect(mm.configs()["failing"].value).toBe(-1);
@@ -326,7 +326,7 @@ describe("ModuleManager", async () => {
const mm = new TestModuleManager(dummyConnection, {
onUpdated: async () => {
mockOnUpdated();
- }
+ },
});
await mm.build();
// @ts-ignore
@@ -342,11 +342,11 @@ describe("ModuleManager", async () => {
const mm = new TestModuleManager(dummyConnection, {
initial: {
// @ts-ignore
- failing: { value: 1 }
+ failing: { value: 1 },
},
onUpdated: async () => {
mockOnUpdated();
- }
+ },
});
await mm.build();
expect(mm.configs()["failing"].value).toBe(1);
@@ -354,7 +354,7 @@ describe("ModuleManager", async () => {
// now safe mutate
// @ts-ignore
expect(mm.mutateConfigSafe("failing").set({ value: -2 })).rejects.toThrow(
- /value must be positive/
+ /value must be positive/,
);
expect(mm.configs()["failing"].value).toBe(1);
expect(mockOnUpdated).toHaveBeenCalled();
diff --git a/app/__test__/modules/migrations/migrations.spec.ts b/app/__test__/modules/migrations/migrations.spec.ts
new file mode 100644
index 0000000..de004ac
--- /dev/null
+++ b/app/__test__/modules/migrations/migrations.spec.ts
@@ -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;
+ 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");
+ });
+});
diff --git a/app/__test__/modules/migrations/samples/v7.json b/app/__test__/modules/migrations/samples/v7.json
new file mode 100644
index 0000000..4ef57cf
--- /dev/null
+++ b/app/__test__/modules/migrations/samples/v7.json
@@ -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": {}
+ }
+}
diff --git a/app/__test__/modules/module-test-suite.ts b/app/__test__/modules/module-test-suite.ts
index d4f0f95..4ad7e5d 100644
--- a/app/__test__/modules/module-test-suite.ts
+++ b/app/__test__/modules/module-test-suite.ts
@@ -19,7 +19,7 @@ export function makeCtx(overrides?: Partial): ModuleBuildCon
guard: new Guard(),
flags: Module.ctx_flags,
logger: new DebugLogger(false),
- ...overrides
+ ...overrides,
};
}
diff --git a/app/__test__/ui/json-form.spec.ts b/app/__test__/ui/json-form.spec.ts
index 2fa535a..c1331f2 100644
--- a/app/__test__/ui/json-form.spec.ts
+++ b/app/__test__/ui/json-form.spec.ts
@@ -16,7 +16,7 @@ describe("json form", () => {
["0", { type: "boolean" }, false],
["on", { type: "boolean" }, true],
["off", { type: "boolean" }, false],
- ["null", { type: "null" }, null]
+ ["null", { type: "null" }, null],
] satisfies [string, Exclude, any][];
for (const [input, schema, output] of examples) {
@@ -35,7 +35,7 @@ describe("json form", () => {
["array", "array", true],
["object", "array", false],
[["string", "number"], "number", true],
- ["number", ["string", "number"], true]
+ ["number", ["string", "number"], true],
] satisfies [IsTypeType, IsTypeType, boolean][];
for (const [type, schemaType, output] of examples) {
@@ -48,7 +48,7 @@ describe("json form", () => {
["#/nested/property/0/name", "#/nested/property/0"],
["#/nested/property/0", "#/nested/property"],
["#/nested/property", "#/nested"],
- ["#/nested", "#"]
+ ["#/nested", "#"],
];
for (const [input, output] of examples) {
@@ -61,16 +61,16 @@ describe("json form", () => {
[
"#/description",
{ type: "object", properties: { description: { type: "string" } } },
- false
+ false,
],
[
"#/description",
{
type: "object",
required: ["description"],
- properties: { description: { type: "string" } }
+ properties: { description: { type: "string" } },
},
- true
+ true,
],
[
"#/nested/property",
@@ -79,11 +79,11 @@ describe("json form", () => {
properties: {
nested: {
type: "object",
- properties: { property: { type: "string" } }
- }
- }
+ properties: { property: { type: "string" } },
+ },
+ },
},
- false
+ false,
],
[
"#/nested/property",
@@ -93,12 +93,12 @@ describe("json form", () => {
nested: {
type: "object",
required: ["property"],
- properties: { property: { type: "string" } }
- }
- }
+ properties: { property: { type: "string" } },
+ },
+ },
},
- true
- ]
+ true,
+ ],
] satisfies [string, Exclude, boolean][];
for (const [pointer, schema, output] of examples) {
@@ -113,7 +113,7 @@ describe("json form", () => {
["tags", "0", "0.tags"],
["tags", 0, "0.tags"],
["nested.property", "prefix", "prefix.nested.property"],
- ["nested.property", "", "nested.property"]
+ ["nested.property", "", "nested.property"],
] satisfies [string, any, string][];
for (const [path, prefix, output] of examples) {
@@ -128,7 +128,7 @@ describe("json form", () => {
["tags", "0", "tags.0"],
["tags", 0, "tags.0"],
["nested.property", "suffix", "nested.property.suffix"],
- ["nested.property", "", "nested.property"]
+ ["nested.property", "", "nested.property"],
] satisfies [string, any, string][];
for (const [path, suffix, output] of examples) {
diff --git a/app/bknd.config.js.ignore b/app/bknd.config.js.ignore
deleted file mode 100644
index 5f279d3..0000000
--- a/app/bknd.config.js.ignore
+++ /dev/null
@@ -1,13 +0,0 @@
-//import type { BkndConfig } from "./src";
-
-export default {
- app: {
- connection: {
- type: "libsql",
- config: {
- //url: "http://localhost:8080"
- url: ":memory:"
- }
- }
- }
-};
diff --git a/app/build.esbuild.ts b/app/build.esbuild.ts
deleted file mode 100644
index 6cb1e13..0000000
--- a/app/build.esbuild.ts
+++ /dev/null
@@ -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> & { 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 = {
- 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 = {};
- 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 = {}): 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()]);
-}
diff --git a/app/build.ts b/app/build.ts
index ba14bc5..0022a80 100644
--- a/app/build.ts
+++ b/app/build.ts
@@ -27,9 +27,9 @@ function buildTypes() {
onExit: () => {
console.log("Types aliased");
types_running = false;
- }
+ },
});
- }
+ },
});
}
@@ -46,41 +46,52 @@ if (types && !watch) {
buildTypes();
}
+function banner(title: string) {
+ console.log("");
+ console.log("=".repeat(40));
+ console.log(title.toUpperCase());
+ console.log("-".repeat(40));
+}
+
/**
* Building backend and general API
*/
async function buildApi() {
+ banner("Building API");
await tsup.build({
minify,
sourcemap,
watch,
entry: ["src/index.ts", "src/data/index.ts", "src/core/index.ts", "src/core/utils/index.ts"],
outDir: "dist",
- external: ["bun:test", "@libsql/client", "bknd/client"],
+ external: ["bun:test", "@libsql/client"],
metafile: true,
platform: "browser",
format: ["esm"],
splitting: false,
treeshake: true,
loader: {
- ".svg": "dataurl"
+ ".svg": "dataurl",
},
onSuccess: async () => {
delayTypes();
- }
+ },
});
}
+async function rewriteClient(path: string) {
+ const bundle = await Bun.file(path).text();
+ await Bun.write(path, '"use client";\n' + bundle.replaceAll("ui/client", "bknd/client"));
+}
+
/**
* Building UI for direct imports
*/
async function buildUi() {
- await tsup.build({
+ const base = {
minify,
sourcemap,
watch,
- entry: ["src/ui/index.ts", "src/ui/client/index.ts", "src/ui/main.css", "src/ui/styles.css"],
- outDir: "dist/ui",
external: [
"bun:test",
"react",
@@ -90,7 +101,7 @@ async function buildUi() {
"use-sync-external-store",
/codemirror/,
"@xyflow/react",
- "@mantine/core"
+ "@mantine/core",
],
metafile: true,
platform: "browser",
@@ -99,14 +110,33 @@ async function buildUi() {
bundle: true,
treeshake: true,
loader: {
- ".svg": "dataurl"
+ ".svg": "dataurl",
},
esbuildOptions: (options) => {
options.logLevel = "silent";
},
+ } satisfies tsup.Options;
+
+ banner("Building UI");
+ await tsup.build({
+ ...base,
+ entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"],
+ outDir: "dist/ui",
onSuccess: async () => {
+ await rewriteClient("./dist/ui/index.js");
delayTypes();
- }
+ },
+ });
+
+ banner("Building Client");
+ await tsup.build({
+ ...base,
+ entry: ["src/ui/client/index.ts"],
+ outDir: "dist/ui/client",
+ onSuccess: async () => {
+ await rewriteClient("./dist/ui/client/index.js");
+ delayTypes();
+ },
});
}
@@ -116,6 +146,7 @@ async function buildUi() {
* - ui/client is external, and after built replaced with "bknd/client"
*/
async function buildUiElements() {
+ banner("Building UI Elements");
await tsup.build({
minify,
sourcemap,
@@ -128,7 +159,7 @@ async function buildUiElements() {
"react-dom",
"react/jsx-runtime",
"react/jsx-dev-runtime",
- "use-sync-external-store"
+ "use-sync-external-store",
],
metafile: true,
platform: "browser",
@@ -137,22 +168,18 @@ async function buildUiElements() {
bundle: true,
treeshake: true,
loader: {
- ".svg": "dataurl"
+ ".svg": "dataurl",
},
esbuildOptions: (options) => {
options.alias = {
// not important for elements, mock to reduce bundle
- "tailwind-merge": "./src/ui/elements/mocks/tailwind-merge.ts"
+ "tailwind-merge": "./src/ui/elements/mocks/tailwind-merge.ts",
};
},
onSuccess: async () => {
- // manually replace ui/client with bknd/client
- const path = "./dist/ui/elements/index.js";
- const bundle = await Bun.file(path).text();
- await Bun.write(path, bundle.replaceAll("ui/client", "bknd/client"));
-
+ await rewriteClient("./dist/ui/elements/index.js");
delayTypes();
- }
+ },
});
}
@@ -176,49 +203,51 @@ function baseConfig(adapter: string, overrides: Partial = {}): tsu
...overrides,
define: {
__isDev: "0",
- ...overrides.define
+ ...overrides.define,
},
external: [
/^cloudflare*/,
/^@?(hono|libsql).*?/,
/^(bknd|react|next|node).*?/,
/.*\.(html)$/,
- ...(Array.isArray(overrides.external) ? overrides.external : [])
- ]
+ ...(Array.isArray(overrides.external) ? overrides.external : []),
+ ],
};
}
async function buildAdapters() {
+ banner("Building Adapters");
// base adapter handles
await tsup.build({
...baseConfig(""),
entry: ["src/adapter/index.ts"],
- outDir: "dist/adapter"
+ outDir: "dist/adapter",
});
// specific adatpers
await tsup.build(baseConfig("remix"));
await tsup.build(baseConfig("bun"));
await tsup.build(baseConfig("astro"));
+ await tsup.build(baseConfig("aws"));
await tsup.build(
baseConfig("cloudflare", {
- external: [/^kysely/]
- })
+ external: [/^kysely/],
+ }),
);
await tsup.build({
...baseConfig("vite"),
- platform: "node"
+ platform: "node",
});
await tsup.build({
...baseConfig("nextjs"),
- platform: "node"
+ platform: "node",
});
await tsup.build({
...baseConfig("node"),
- platform: "node"
+ platform: "node",
});
}
diff --git a/app/internal/esbuild.entry-output-meta.plugin.ts b/app/internal/esbuild.entry-output-meta.plugin.ts
index 6bd3ab4..0521c51 100644
--- a/app/internal/esbuild.entry-output-meta.plugin.ts
+++ b/app/internal/esbuild.entry-output-meta.plugin.ts
@@ -5,8 +5,8 @@ export const entryOutputMeta = (
outputs: {
output: string;
meta: Metafile["outputs"][string];
- }[]
- ) => void | Promise
+ }[],
+ ) => void | Promise,
): Plugin => ({
name: "report-entry-output-plugin",
setup(build) {
@@ -29,5 +29,5 @@ export const entryOutputMeta = (
await onComplete?.(outputs);
}
});
- }
+ },
});
diff --git a/app/package.json b/app/package.json
index 002ae21..442c9a0 100644
--- a/app/package.json
+++ b/app/package.json
@@ -3,7 +3,7 @@
"type": "module",
"sideEffects": false,
"bin": "./dist/cli/index.js",
- "version": "0.8.1",
+ "version": "0.9.1",
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
"homepage": "https://bknd.io",
"repository": {
@@ -32,79 +32,83 @@
},
"license": "FSL-1.1-MIT",
"dependencies": {
- "@cfworker/json-schema": "^2.0.1",
+ "@cfworker/json-schema": "^4.1.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-json": "^6.0.1",
- "@codemirror/lang-liquid": "^6.2.1",
- "@hello-pangea/dnd": "^17.0.0",
+ "@codemirror/lang-liquid": "^6.2.2",
+ "@hello-pangea/dnd": "^18.0.1",
"@libsql/client": "^0.14.0",
- "@mantine/core": "^7.13.4",
- "@sinclair/typebox": "^0.32.34",
- "@tanstack/react-form": "0.19.2",
- "@uiw/react-codemirror": "^4.23.6",
- "@xyflow/react": "^12.3.2",
- "aws4fetch": "^1.0.18",
+ "@mantine/core": "^7.17.1",
+ "@mantine/hooks": "^7.17.1",
+ "@sinclair/typebox": "^0.34.30",
+ "@tanstack/react-form": "^1.0.5",
+ "@uiw/react-codemirror": "^4.23.10",
+ "@xyflow/react": "^12.4.4",
+ "aws4fetch": "^1.0.20",
"dayjs": "^1.11.13",
- "fast-xml-parser": "^4.4.0",
- "hono": "^4.6.12",
+ "fast-xml-parser": "^5.0.8",
+ "hono": "^4.7.4",
"json-schema-form-react": "^0.0.2",
"json-schema-library": "^10.0.0-rc7",
"json-schema-to-ts": "^3.1.1",
- "kysely": "^0.27.4",
- "liquidjs": "^10.15.0",
+ "kysely": "^0.27.6",
+ "liquidjs": "^10.21.0",
"lodash-es": "^4.17.21",
"oauth4webapi": "^2.11.1",
"object-path-immutable": "^4.1.2",
"picocolors": "^1.1.1",
- "radix-ui": "^1.1.2",
- "swr": "^2.2.5"
+ "radix-ui": "^1.1.3",
+ "swr": "^2.3.3"
},
"devDependencies": {
- "@aws-sdk/client-s3": "^3.613.0",
+ "@aws-sdk/client-s3": "^3.758.0",
"@bluwy/giget-core": "^0.1.2",
"@dagrejs/dagre": "^1.1.4",
- "@hono/typebox-validator": "^0.2.6",
- "@hono/vite-dev-server": "^0.17.0",
- "@hono/zod-validator": "^0.4.1",
- "@hookform/resolvers": "^3.9.1",
+ "@hono/typebox-validator": "^0.3.2",
+ "@hono/vite-dev-server": "^0.19.0",
+ "@hookform/resolvers": "^4.1.3",
"@libsql/kysely-libsql": "^0.4.1",
- "@rjsf/core": "^5.22.2",
+ "@mantine/modals": "^7.17.1",
+ "@mantine/notifications": "^7.17.1",
+ "@rjsf/core": "5.22.2",
"@tabler/icons-react": "3.18.0",
- "@types/node": "^22.10.0",
- "@types/react": "^18.3.12",
- "@types/react-dom": "^18.3.1",
- "@vitejs/plugin-react": "^4.3.3",
- "autoprefixer": "^10.4.20",
+ "@tailwindcss/postcss": "^4.0.12",
+ "@tailwindcss/vite": "^4.0.12",
+ "@types/node": "^22.13.10",
+ "@types/react": "^19.0.10",
+ "@types/react-dom": "^19.0.4",
+ "@vitejs/plugin-react": "^4.3.4",
+ "autoprefixer": "^10.4.21",
"clsx": "^2.1.1",
"dotenv": "^16.4.7",
- "esbuild-postcss": "^0.0.4",
- "jotai": "^2.10.1",
+ "jotai": "^2.12.2",
"kysely-d1": "^0.3.0",
"open": "^10.1.0",
"openapi-types": "^12.1.3",
- "postcss": "^8.4.47",
+ "postcss": "^8.5.3",
"postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1",
- "react-hook-form": "^7.53.1",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "react-hook-form": "^7.54.2",
"react-icons": "5.2.1",
- "react-json-view-lite": "^2.0.1",
- "sql-formatter": "^15.4.9",
- "tailwind-merge": "^2.5.4",
- "tailwindcss": "^3.4.14",
+ "react-json-view-lite": "^2.4.1",
+ "sql-formatter": "^15.4.11",
+ "tailwind-merge": "^3.0.2",
+ "tailwindcss": "^4.0.12",
"tailwindcss-animate": "^1.0.7",
- "tsc-alias": "^1.8.10",
- "tsup": "^8.3.5",
- "vite": "^5.4.10",
- "vite-plugin-static-copy": "^2.0.0",
- "vite-tsconfig-paths": "^5.0.1",
- "wouter": "^3.3.5"
+ "tsc-alias": "^1.8.11",
+ "tsup": "^8.4.0",
+ "vite": "^6.2.1",
+ "vite-tsconfig-paths": "^5.1.4",
+ "wouter": "^3.6.0"
},
"optionalDependencies": {
- "@hono/node-server": "^1.13.7"
+ "@hono/node-server": "^1.13.8"
},
"peerDependencies": {
- "react": ">=18",
- "react-dom": ">=18"
+ "react": "^19.x",
+ "react-dom": "^19.x"
},
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -189,6 +193,11 @@
"import": "./dist/adapter/astro/index.js",
"require": "./dist/adapter/astro/index.cjs"
},
+ "./adapter/aws": {
+ "types": "./dist/types/adapter/aws/index.d.ts",
+ "import": "./dist/adapter/aws/index.js",
+ "require": "./dist/adapter/aws/index.cjs"
+ },
"./dist/main.css": "./dist/ui/main.css",
"./dist/styles.css": "./dist/ui/styles.css",
"./dist/manifest.json": "./dist/static/.vite/manifest.json"
@@ -221,4 +230,4 @@
"bun",
"node"
]
-}
\ No newline at end of file
+}
diff --git a/app/postcss.config.js b/app/postcss.config.js
index 0c65377..61ce526 100644
--- a/app/postcss.config.js
+++ b/app/postcss.config.js
@@ -1,9 +1,6 @@
export default {
plugins: {
- "postcss-import": {},
- "tailwindcss/nesting": {},
- tailwindcss: {},
- autoprefixer: {},
+ "@tailwindcss/postcss": {},
"postcss-preset-mantine": {},
"postcss-simple-vars": {
variables: {
@@ -11,8 +8,8 @@ export default {
"mantine-breakpoint-sm": "48em",
"mantine-breakpoint-md": "62em",
"mantine-breakpoint-lg": "75em",
- "mantine-breakpoint-xl": "88em"
- }
- }
- }
+ "mantine-breakpoint-xl": "88em",
+ },
+ },
+ },
};
diff --git a/app/src/Api.ts b/app/src/Api.ts
index 2e35cee..70cbd13 100644
--- a/app/src/Api.ts
+++ b/app/src/Api.ts
@@ -2,9 +2,9 @@ import type { SafeUser } from "auth";
import { AuthApi } from "auth/api/AuthApi";
import { DataApi } from "data/api/DataApi";
import { decode } from "hono/jwt";
-import { omit } from "lodash-es";
import { MediaApi } from "media/api/MediaApi";
import { SystemApi } from "modules/SystemApi";
+import { omitKeys } from "core/utils";
export type TApiUser = SafeUser;
@@ -122,7 +122,7 @@ export class Api {
this.verified = false;
if (token) {
- this.user = omit(decode(token).payload as any, ["iat", "iss", "exp"]) as any;
+ this.user = omitKeys(decode(token).payload as any, ["iat", "iss", "exp"]) as any;
} else {
this.user = undefined;
}
@@ -153,7 +153,7 @@ export class Api {
return {
token: this.token,
user: this.user,
- verified: this.verified
+ verified: this.verified,
};
}
@@ -198,7 +198,7 @@ export class Api {
token: this.token,
headers: this.options.headers,
token_transport: this.token_transport,
- verbose: this.options.verbose
+ verbose: this.options.verbose,
});
}
@@ -211,9 +211,9 @@ export class Api {
this.auth = new AuthApi(
{
...baseParams,
- onTokenUpdate: (token) => this.updateToken(token, true)
+ onTokenUpdate: (token) => this.updateToken(token, true),
},
- fetcher
+ fetcher,
);
this.media = new MediaApi(baseParams, fetcher);
}
diff --git a/app/src/App.ts b/app/src/App.ts
index 868b11a..ac0ea1d 100644
--- a/app/src/App.ts
+++ b/app/src/App.ts
@@ -1,4 +1,3 @@
-import { Api, type ApiOptions } from "Api";
import type { CreateUserPayload } from "auth/AppAuth";
import { $console } from "core";
import { Event } from "core/events";
@@ -9,12 +8,15 @@ import {
type ModuleBuildContext,
ModuleManager,
type ModuleManagerOptions,
- type Modules
+ type Modules,
} from "modules/ModuleManager";
import * as SystemPermissions from "modules/permissions";
import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
import { SystemController } from "modules/server/SystemController";
+// biome-ignore format: must be there
+import { Api, type ApiOptions } from "Api";
+
export type AppPlugin = (app: App) => Promise | void;
abstract class AppEvent extends Event<{ app: App } & A> {}
@@ -31,7 +33,7 @@ export const AppEvents = { AppConfigUpdatedEvent, AppBuiltEvent, AppFirstBoot }
export type AppOptions = {
plugins?: AppPlugin[];
- seed?: (ctx: ModuleBuildContext) => Promise;
+ seed?: (ctx: ModuleBuildContext & { app: App }) => Promise;
manager?: Omit;
};
export type CreateAppConfig = {
@@ -48,6 +50,7 @@ export type CreateAppConfig = {
};
export type AppConfig = InitialModuleConfigs;
+export type LocalApiOptions = Request | ApiOptions;
export class App {
modules: ModuleManager;
@@ -55,17 +58,18 @@ export class App {
adminController?: AdminController;
private trigger_first_boot = false;
private plugins: AppPlugin[];
+ private _id: string = crypto.randomUUID();
+ private _building: boolean = false;
constructor(
private connection: Connection,
_initialConfig?: InitialModuleConfigs,
- private options?: AppOptions
+ private options?: AppOptions,
) {
this.plugins = options?.plugins ?? [];
this.modules = new ModuleManager(connection, {
...(options?.manager ?? {}),
initial: _initialConfig,
- seed: options?.seed,
onUpdated: async (key, config) => {
// if the EventManager was disabled, we assume we shouldn't
// respond to events, such as "onUpdated".
@@ -88,8 +92,13 @@ export class App {
server.use(async (c, next) => {
c.set("app", this);
await next();
+
+ try {
+ // gracefully add the app id
+ c.res.headers.set("X-bknd-id", this._id);
+ } catch (e) {}
});
- }
+ },
});
this.modules.ctx().emgr.registerEvents(AppEvents);
}
@@ -98,9 +107,18 @@ export class App {
return this.modules.ctx().emgr;
}
- async build(options?: { sync?: boolean }) {
+ async build(options?: { sync?: boolean; fetch?: boolean; forceBuild?: boolean }) {
+ // prevent multiple concurrent builds
+ if (this._building) {
+ while (this._building) {
+ await new Promise((resolve) => setTimeout(resolve, 10));
+ }
+ if (!options?.forceBuild) return;
+ }
+ this._building = true;
+
if (options?.sync) this.modules.ctx().flags.sync_required = true;
- await this.modules.build();
+ await this.modules.build({ fetch: options?.fetch });
const { guard, server } = this.modules.ctx();
@@ -113,13 +131,20 @@ export class App {
await Promise.all(this.plugins.map((plugin) => plugin(this)));
}
+ $console.log("App built");
await this.emgr.emit(new AppBuiltEvent({ app: this }));
// first boot is set from ModuleManager when there wasn't a config table
if (this.trigger_first_boot) {
this.trigger_first_boot = false;
await this.emgr.emit(new AppFirstBoot({ app: this }));
+ await this.options?.seed?.({
+ ...this.modules.ctx(),
+ app: this,
+ });
}
+
+ this._building = false;
}
mutateConfig(module: Module) {
@@ -144,8 +169,8 @@ export class App {
{
get: (_, module: keyof Modules) => {
return this.modules.get(module);
- }
- }
+ },
+ },
) as Modules;
}
@@ -180,13 +205,13 @@ export class App {
return this.module.auth.createUser(p);
}
- getApi(options: Request | ApiOptions = {}) {
+ getApi(options?: LocalApiOptions) {
const fetcher = this.server.request as typeof fetch;
- if (options instanceof Request) {
+ if (options && options instanceof Request) {
return new Api({ request: options, headers: options.headers, fetcher });
}
- return new Api({ host: "http://localhost", ...options, fetcher });
+ return new Api({ host: "http://localhost", ...(options ?? {}), fetcher });
}
}
@@ -199,7 +224,7 @@ export function createApp(config: CreateAppConfig = {}) {
} else if (typeof config.connection === "object") {
if ("type" in config.connection) {
$console.warn(
- "Using deprecated connection type 'libsql', use the 'config' object directly."
+ "Using deprecated connection type 'libsql', use the 'config' object directly.",
);
connection = new LibsqlConnection(config.connection.config);
} else {
diff --git a/app/src/adapter/astro/astro.adapter.ts b/app/src/adapter/astro/astro.adapter.ts
index c15d570..61971e3 100644
--- a/app/src/adapter/astro/astro.adapter.ts
+++ b/app/src/adapter/astro/astro.adapter.ts
@@ -17,7 +17,7 @@ export type Options = {
export async function getApi(Astro: TAstro, options: Options = { mode: "static" }) {
const api = new Api({
host: new URL(Astro.request.url).origin,
- headers: options.mode === "dynamic" ? Astro.request.headers : undefined
+ headers: options.mode === "dynamic" ? Astro.request.headers : undefined,
});
await api.verifyAuth();
return api;
diff --git a/app/src/adapter/aws/aws-lambda.adapter.ts b/app/src/adapter/aws/aws-lambda.adapter.ts
new file mode 100644
index 0000000..9488065
--- /dev/null
+++ b/app/src/adapter/aws/aws-lambda.adapter.ts
@@ -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 = {
+ 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);
+ };
+}
diff --git a/app/src/adapter/aws/index.ts b/app/src/adapter/aws/index.ts
new file mode 100644
index 0000000..9c07f2b
--- /dev/null
+++ b/app/src/adapter/aws/index.ts
@@ -0,0 +1 @@
+export * from "./aws-lambda.adapter";
diff --git a/app/src/adapter/bun/bun.adapter.ts b/app/src/adapter/bun/bun.adapter.ts
index 851c54e..2087c5e 100644
--- a/app/src/adapter/bun/bun.adapter.ts
+++ b/app/src/adapter/bun/bun.adapter.ts
@@ -19,7 +19,7 @@ export async function createApp({ distPath, ...config }: RuntimeBkndConfig = {})
registerLocalMediaAdapter();
app = await createRuntimeApp({
...config,
- serveStatic: serveStatic({ root })
+ serveStatic: serveStatic({ root }),
});
}
@@ -34,6 +34,7 @@ export function serve({
port = config.server.default_port,
onBuilt,
buildConfig,
+ adminOptions,
...serveOptions
}: BunBkndConfig = {}) {
Bun.serve({
@@ -46,10 +47,11 @@ export function serve({
options,
onBuilt,
buildConfig,
- distPath
+ adminOptions,
+ distPath,
});
return app.fetch(request);
- }
+ },
});
console.log(`Server is running on http://localhost:${port}`);
diff --git a/app/src/adapter/cloudflare/D1Connection.ts b/app/src/adapter/cloudflare/D1Connection.ts
index 810cca8..768ca44 100644
--- a/app/src/adapter/cloudflare/D1Connection.ts
+++ b/app/src/adapter/cloudflare/D1Connection.ts
@@ -12,7 +12,7 @@ export type D1ConnectionConfig = {
class CustomD1Dialect extends D1Dialect {
override createIntrospector(db: Kysely): DatabaseIntrospector {
return new SqliteIntrospector(db, {
- excludeTables: ["_cf_KV"]
+ excludeTables: ["_cf_KV"],
});
}
}
@@ -23,7 +23,7 @@ export class D1Connection extends SqliteConnection {
const kysely = new Kysely({
dialect: new CustomD1Dialect({ database: config.binding }),
- plugins
+ plugins,
});
super(kysely, {}, plugins);
}
@@ -37,7 +37,7 @@ export class D1Connection extends SqliteConnection {
}
protected override async batch(
- queries: [...Queries]
+ queries: [...Queries],
): Promise<{
[K in keyof Queries]: Awaited>;
}> {
@@ -47,7 +47,7 @@ export class D1Connection extends SqliteConnection {
queries.map((q) => {
const { sql, parameters } = q.compile();
return db.prepare(sql).bind(...parameters);
- })
+ }),
);
// let it run through plugins
diff --git a/app/src/adapter/cloudflare/StorageR2Adapter.ts b/app/src/adapter/cloudflare/StorageR2Adapter.ts
index aedeb10..5432e79 100644
--- a/app/src/adapter/cloudflare/StorageR2Adapter.ts
+++ b/app/src/adapter/cloudflare/StorageR2Adapter.ts
@@ -8,9 +8,9 @@ import { getBindings } from "./bindings";
export function makeSchema(bindings: string[] = []) {
return Type.Object(
{
- binding: bindings.length > 0 ? StringEnum(bindings) : Type.Optional(Type.String())
+ binding: bindings.length > 0 ? StringEnum(bindings) : Type.Optional(Type.String()),
},
- { title: "R2", description: "Cloudflare R2 storage" }
+ { title: "R2", description: "Cloudflare R2 storage" },
);
}
@@ -36,10 +36,10 @@ export function registerMedia(env: Record) {
override toJSON() {
return {
...super.toJSON(),
- config: this.config
+ config: this.config,
};
}
- }
+ },
);
}
@@ -67,13 +67,13 @@ export class StorageR2Adapter implements StorageAdapter {
}
}
async listObjects(
- prefix?: string
+ prefix?: string,
): Promise<{ key: string; last_modified: Date; size: number }[]> {
const list = await this.bucket.list({ limit: 50 });
return list.objects.map((item) => ({
key: item.key,
size: item.size,
- last_modified: item.uploaded
+ last_modified: item.uploaded,
}));
}
@@ -89,7 +89,7 @@ export class StorageR2Adapter implements StorageAdapter {
let object: R2ObjectBody | null;
const responseHeaders = new Headers({
"Accept-Ranges": "bytes",
- "Content-Type": guess(key)
+ "Content-Type": guess(key),
});
//console.log("getObject:headers", headersToObject(headers));
@@ -98,7 +98,7 @@ export class StorageR2Adapter implements StorageAdapter {
? {} // miniflare doesn't support range requests
: {
range: headers,
- onlyIf: headers
+ onlyIf: headers,
};
object = (await this.bucket.get(key, options)) as R2ObjectBody;
@@ -130,7 +130,7 @@ export class StorageR2Adapter implements StorageAdapter {
return new Response(object.body, {
status: object.range ? 206 : 200,
- headers: responseHeaders
+ headers: responseHeaders,
});
}
@@ -139,7 +139,7 @@ export class StorageR2Adapter implements StorageAdapter {
if (!metadata || Object.keys(metadata).length === 0) {
// guessing is especially required for dev environment (miniflare)
metadata = {
- contentType: guess(object.key)
+ contentType: guess(object.key),
};
}
@@ -157,7 +157,7 @@ export class StorageR2Adapter implements StorageAdapter {
return {
type: String(head.httpMetadata?.contentType ?? guess(key)),
- size: head.size
+ size: head.size,
};
}
@@ -172,7 +172,7 @@ export class StorageR2Adapter implements StorageAdapter {
toJSON(secrets?: boolean) {
return {
type: this.getName(),
- config: {}
+ config: {},
};
}
}
diff --git a/app/src/adapter/cloudflare/bindings.ts b/app/src/adapter/cloudflare/bindings.ts
index 491d0a8..82eca2a 100644
--- a/app/src/adapter/cloudflare/bindings.ts
+++ b/app/src/adapter/cloudflare/bindings.ts
@@ -15,7 +15,7 @@ export function getBindings(env: any, type: T): Bindin
if (env[key] && (env[key] as any).constructor.name === type) {
bindings.push({
key,
- value: env[key] as BindingTypeMap[T]
+ value: env[key] as BindingTypeMap[T],
});
}
} catch (e) {}
diff --git a/app/src/adapter/cloudflare/cloudflare-workers.adapter.ts b/app/src/adapter/cloudflare/cloudflare-workers.adapter.ts
index 4486609..7483d52 100644
--- a/app/src/adapter/cloudflare/cloudflare-workers.adapter.ts
+++ b/app/src/adapter/cloudflare/cloudflare-workers.adapter.ts
@@ -84,7 +84,7 @@ export function serve(config: CloudflareBkndConfig = {}) {
hono.all("*", async (c, next) => {
const res = await serveStatic({
path: `./${pathname}`,
- manifest: config.manifest!
+ manifest: config.manifest!,
})(c as any, next);
if (res instanceof Response) {
const ttl = 60 * 60 * 24 * 365;
@@ -114,6 +114,6 @@ export function serve(config: CloudflareBkndConfig = {}) {
default:
throw new Error(`Unknown mode ${mode}`);
}
- }
+ },
};
}
diff --git a/app/src/adapter/cloudflare/index.ts b/app/src/adapter/cloudflare/index.ts
index a10dc53..e89198e 100644
--- a/app/src/adapter/cloudflare/index.ts
+++ b/app/src/adapter/cloudflare/index.ts
@@ -10,7 +10,7 @@ export {
getBindings,
type BindingTypeMap,
type GetBindingType,
- type BindingMap
+ type BindingMap,
} from "./bindings";
export function d1(config: D1ConnectionConfig) {
diff --git a/app/src/adapter/cloudflare/modes/cached.ts b/app/src/adapter/cloudflare/modes/cached.ts
index 48f6926..c126ff7 100644
--- a/app/src/adapter/cloudflare/modes/cached.ts
+++ b/app/src/adapter/cloudflare/modes/cached.ts
@@ -31,13 +31,13 @@ export async function getCached(config: CloudflareBkndConfig, { env, ctx, ...arg
async ({ params: { app } }) => {
saveConfig(app.toJSON(true));
},
- "sync"
+ "sync",
);
await config.beforeBuild?.(app);
},
- adminOptions: { html: config.html }
+ adminOptions: { html: config.html },
},
- { env, ctx, ...args }
+ { env, ctx, ...args },
);
if (!cachedConfig) {
diff --git a/app/src/adapter/cloudflare/modes/durable.ts b/app/src/adapter/cloudflare/modes/durable.ts
index bd58f85..63fce34 100644
--- a/app/src/adapter/cloudflare/modes/durable.ts
+++ b/app/src/adapter/cloudflare/modes/durable.ts
@@ -23,7 +23,7 @@ export async function getDurable(config: CloudflareBkndConfig, ctx: Context) {
config: create_config,
html: config.html,
keepAliveSeconds: config.keepAliveSeconds,
- setAdminHtml: config.setAdminHtml
+ setAdminHtml: config.setAdminHtml,
});
const headers = new Headers(res.headers);
@@ -32,7 +32,7 @@ export async function getDurable(config: CloudflareBkndConfig, ctx: Context) {
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
- headers
+ headers,
});
}
@@ -48,7 +48,7 @@ export class DurableBkndApp extends DurableObject {
html?: string;
keepAliveSeconds?: number;
setAdminHtml?: boolean;
- }
+ },
) {
let buildtime = 0;
if (!this.app) {
@@ -73,7 +73,7 @@ export class DurableBkndApp extends DurableObject {
return c.json({
id: this.id,
keepAliveSeconds: options?.keepAliveSeconds ?? 0,
- colo: context.colo
+ colo: context.colo,
});
});
@@ -82,7 +82,7 @@ export class DurableBkndApp extends DurableObject {
adminOptions: { html: options.html },
beforeBuild: async (app) => {
await this.beforeBuild(app);
- }
+ },
});
buildtime = performance.now() - start;
@@ -101,7 +101,7 @@ export class DurableBkndApp extends DurableObject {
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
- headers
+ headers,
});
}
diff --git a/app/src/adapter/cloudflare/modes/fresh.ts b/app/src/adapter/cloudflare/modes/fresh.ts
index 7a2af67..b13c537 100644
--- a/app/src/adapter/cloudflare/modes/fresh.ts
+++ b/app/src/adapter/cloudflare/modes/fresh.ts
@@ -6,9 +6,9 @@ export async function makeApp(config: CloudflareBkndConfig, ctx: Context) {
return await createRuntimeApp(
{
...makeCfConfig(config, ctx),
- adminOptions: config.html ? { html: config.html } : undefined
+ adminOptions: config.html ? { html: config.html } : undefined,
},
- ctx
+ ctx,
);
}
diff --git a/app/src/adapter/index.ts b/app/src/adapter/index.ts
index 1c48f8b..6d416e9 100644
--- a/app/src/adapter/index.ts
+++ b/app/src/adapter/index.ts
@@ -14,6 +14,8 @@ export type FrameworkBkndConfig = BkndConfig;
export type RuntimeBkndConfig = BkndConfig & {
distPath?: string;
+ serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
+ adminOptions?: AdminControllerOptions | false;
};
export function makeConfig(config: BkndConfig, args?: Args): CreateAppConfig {
@@ -34,7 +36,7 @@ export function makeConfig(config: BkndConfig, args?: Args): C
export async function createFrameworkApp(
config: FrameworkBkndConfig,
- args?: Args
+ args?: Args,
): Promise {
const app = App.create(makeConfig(config, args));
@@ -44,7 +46,7 @@ export async function createFrameworkApp(
async () => {
await config.onBuilt?.(app);
},
- "sync"
+ "sync",
);
}
@@ -55,15 +57,8 @@ export async function createFrameworkApp(
}
export async function createRuntimeApp(
- {
- serveStatic,
- adminOptions,
- ...config
- }: RuntimeBkndConfig & {
- serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
- adminOptions?: AdminControllerOptions | false;
- },
- env?: Env
+ { serveStatic, adminOptions, ...config }: RuntimeBkndConfig,
+ env?: Env,
): Promise {
const app = App.create(makeConfig(config, env));
@@ -82,7 +77,7 @@ export async function createRuntimeApp(
app.registerAdminController(adminOptions);
}
},
- "sync"
+ "sync",
);
await config.beforeBuild?.(app);
diff --git a/app/src/adapter/nextjs/nextjs.adapter.ts b/app/src/adapter/nextjs/nextjs.adapter.ts
index adaf853..5c2a364 100644
--- a/app/src/adapter/nextjs/nextjs.adapter.ts
+++ b/app/src/adapter/nextjs/nextjs.adapter.ts
@@ -1,64 +1,60 @@
-import type { IncomingMessage, ServerResponse } from "node:http";
-import { nodeRequestToRequest } from "adapter/utils";
import type { App } from "bknd";
import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter";
-import { Api } from "bknd/client";
+import { getRuntimeKey, isNode } from "core/utils";
export type NextjsBkndConfig = FrameworkBkndConfig & {
- cleanSearch?: string[];
+ cleanRequest?: { searchParams?: string[] };
};
-type GetServerSidePropsContext = {
- req: IncomingMessage;
- res: ServerResponse;
- params?: Params;
- query: any;
- preview?: boolean;
- previewData?: any;
- draftMode?: boolean;
- resolvedUrl: string;
- locale?: string;
- locales?: string[];
- defaultLocale?: string;
-};
+let app: App;
+let building: boolean = false;
-export function createApi({ req }: GetServerSidePropsContext) {
- const request = nodeRequestToRequest(req);
- return new Api({
- host: new URL(request.url).origin,
- headers: request.headers
- });
+export async function getApp(config: NextjsBkndConfig) {
+ if (building) {
+ while (building) {
+ await new Promise((resolve) => setTimeout(resolve, 5));
+ }
+ if (app) return app;
+ }
+
+ building = true;
+ if (!app) {
+ app = await createFrameworkApp(config);
+ await app.build();
+ }
+ building = false;
+ return app;
}
-export function withApi(handler: (ctx: GetServerSidePropsContext & { api: Api }) => T) {
- return async (ctx: GetServerSidePropsContext & { api: Api }) => {
- const api = createApi(ctx);
- await api.verifyAuth();
- return handler({ ...ctx, api });
- };
-}
+function getCleanRequest(req: Request, cleanRequest: NextjsBkndConfig["cleanRequest"]) {
+ if (!cleanRequest) return req;
-function getCleanRequest(
- req: Request,
- { cleanSearch = ["route"] }: Pick
-) {
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(), {
method: req.method,
headers: req.headers,
- body: req.body
+ body: req.body,
});
}
-let app: App;
-export function serve({ cleanSearch, ...config }: NextjsBkndConfig = {}) {
+export function serve({ cleanRequest, ...config }: NextjsBkndConfig = {}) {
return async (req: Request) => {
if (!app) {
- app = await createFrameworkApp(config);
+ app = await getApp(config);
}
- const request = getCleanRequest(req, { cleanSearch });
- return app.fetch(request, process.env);
+ const request = getCleanRequest(req, cleanRequest);
+ return app.fetch(request);
};
}
diff --git a/app/src/adapter/node/index.ts b/app/src/adapter/node/index.ts
index b70a274..5d71d8c 100644
--- a/app/src/adapter/node/index.ts
+++ b/app/src/adapter/node/index.ts
@@ -1,7 +1,7 @@
import { registries } from "bknd";
import {
type LocalAdapterConfig,
- StorageLocalAdapter
+ StorageLocalAdapter,
} from "../../media/storage/adapters/StorageLocalAdapter";
export * from "./node.adapter";
diff --git a/app/src/adapter/node/node.adapter.ts b/app/src/adapter/node/node.adapter.ts
index 326ab92..97a8b82 100644
--- a/app/src/adapter/node/node.adapter.ts
+++ b/app/src/adapter/node/node.adapter.ts
@@ -24,7 +24,7 @@ export function serve({
}: NodeBkndConfig = {}) {
const root = path.relative(
process.cwd(),
- path.resolve(distPath ?? relativeDistPath ?? "./node_modules/bknd/dist", "static")
+ path.resolve(distPath ?? relativeDistPath ?? "./node_modules/bknd/dist", "static"),
);
if (relativeDistPath) {
console.warn("relativeDistPath is deprecated, please use distPath instead");
@@ -41,16 +41,16 @@ export function serve({
registerLocalMediaAdapter();
app = await createRuntimeApp({
...config,
- serveStatic: serveStatic({ root })
+ serveStatic: serveStatic({ root }),
});
}
return app.fetch(req);
- }
+ },
},
(connInfo) => {
console.log(`Server is running on http://localhost:${connInfo.port}`);
listener?.(connInfo);
- }
+ },
);
}
diff --git a/app/src/adapter/remix/remix.adapter.ts b/app/src/adapter/remix/remix.adapter.ts
index 38b81e8..0820133 100644
--- a/app/src/adapter/remix/remix.adapter.ts
+++ b/app/src/adapter/remix/remix.adapter.ts
@@ -10,7 +10,10 @@ type RemixContext = {
let app: App;
let building: boolean = false;
-export async function getApp(config: RemixBkndConfig, args?: RemixContext) {
+export async function getApp(
+ config: RemixBkndConfig,
+ args?: Args
+) {
if (building) {
while (building) {
await new Promise((resolve) => setTimeout(resolve, 5));
@@ -28,10 +31,10 @@ export async function getApp(config: RemixBkndConfig, args?: RemixContext) {
}
export function serve(
- config: RemixBkndConfig = {}
+ config: RemixBkndConfig = {},
) {
return async (args: Args) => {
- app = await createFrameworkApp(config, args);
+ app = await getApp(config, args);
return app.fetch(args.request);
};
}
diff --git a/app/src/adapter/utils.ts b/app/src/adapter/utils.ts
index f804133..b18b4c8 100644
--- a/app/src/adapter/utils.ts
+++ b/app/src/adapter/utils.ts
@@ -20,6 +20,6 @@ export function nodeRequestToRequest(req: IncomingMessage): Request {
const method = req.method || "GET";
return new Request(url, {
method,
- headers
+ headers,
});
}
diff --git a/app/src/adapter/vite/dev-server-config.ts b/app/src/adapter/vite/dev-server-config.ts
index 372e470..9be18fd 100644
--- a/app/src/adapter/vite/dev-server-config.ts
+++ b/app/src/adapter/vite/dev-server-config.ts
@@ -8,7 +8,7 @@ export const devServerConfig = {
/^\/@.+$/,
/\/components.*?\.json.*/, // @todo: improve
/^\/(public|assets|static)\/.+/,
- /^\/node_modules\/.*/
+ /^\/node_modules\/.*/,
] as any,
- injectClientScript: false
+ injectClientScript: false,
} as const;
diff --git a/app/src/adapter/vite/vite.adapter.ts b/app/src/adapter/vite/vite.adapter.ts
index dee0603..bb7eb59 100644
--- a/app/src/adapter/vite/vite.adapter.ts
+++ b/app/src/adapter/vite/vite.adapter.ts
@@ -24,7 +24,7 @@ window.__vite_plugin_react_preamble_installed__ = true
${addBkndContext ? "" : ""}
-`
+`,
);
}
@@ -39,12 +39,12 @@ async function createApp(config: ViteBkndConfig = {}, env?: any) {
: {
html: config.html,
forceDev: config.forceDev ?? {
- mainPath: "/src/main.tsx"
- }
+ mainPath: "/src/main.tsx",
+ },
},
- serveStatic: ["/assets/*", serveStatic({ root: config.distPath ?? "./" })]
+ serveStatic: ["/assets/*", serveStatic({ root: config.distPath ?? "./" })],
},
- env
+ env,
);
}
@@ -53,7 +53,7 @@ export function serveFresh(config: Omit = {}) {
async fetch(request: Request, env: any, ctx: ExecutionContext) {
const app = await createApp(config, env);
return app.fetch(request, env, ctx);
- }
+ },
};
}
@@ -66,7 +66,7 @@ export function serveCached(config: Omit = {}) {
}
return app.fetch(request, env, ctx);
- }
+ },
};
}
@@ -77,6 +77,6 @@ export function serve({ mode, ...config }: ViteBkndConfig = {}) {
export function devServer(options: DevServerOptions) {
return honoViteDevServer({
...devServerConfig,
- ...options
+ ...options,
});
}
diff --git a/app/src/auth/AppAuth.ts b/app/src/auth/AppAuth.ts
index ca5b919..9739566 100644
--- a/app/src/auth/AppAuth.ts
+++ b/app/src/auth/AppAuth.ts
@@ -4,10 +4,10 @@ import {
Authenticator,
type ProfileExchange,
Role,
- type Strategy
+ type Strategy,
} from "auth";
import type { PasswordStrategy } from "auth/authenticate/strategies";
-import { type DB, Exception, type PrimaryFieldType } from "core";
+import { $console, type DB, Exception, type PrimaryFieldType } from "core";
import { type Static, secureRandomString, transformObject } from "core/utils";
import type { Entity, EntityManager } from "data";
import { type FieldSchema, em, entity, enumm, text } from "data/prototype";
@@ -41,6 +41,12 @@ export class AppAuth extends Module {
}
}
+ // @todo: password strategy is required atm
+ if (!to.strategies?.password?.enabled) {
+ $console.warn("Password strategy cannot be disabled.");
+ to.strategies!.password!.enabled = true;
+ }
+
return to;
}
@@ -56,7 +62,6 @@ export class AppAuth extends Module {
// register roles
const roles = transformObject(this.config.roles ?? {}, (role, name) => {
- //console.log("role", role, name);
return Role.create({ name, ...role });
});
this.ctx.guard.setRoles(Object.values(roles));
@@ -69,15 +74,15 @@ export class AppAuth extends Module {
} catch (e) {
throw new Error(
`Could not build strategy ${String(
- name
- )} with config ${JSON.stringify(strategy.config)}`
+ name,
+ )} with config ${JSON.stringify(strategy.config)}`,
);
}
});
this._authenticator = new Authenticator(strategies, this.resolveUser.bind(this), {
jwt: this.config.jwt,
- cookie: this.config.cookie
+ cookie: this.config.cookie,
});
this.registerEntities();
@@ -88,6 +93,14 @@ export class AppAuth extends Module {
this.ctx.guard.registerPermissions(Object.values(AuthPermissions));
}
+ isStrategyEnabled(strategy: Strategy | string) {
+ const name = typeof strategy === "string" ? strategy : strategy.getName();
+ // for now, password is always active
+ if (name === "password") return true;
+
+ return this.config.strategies?.[name]?.enabled ?? false;
+ }
+
get controller(): AuthController {
if (!this.isBuilt()) {
throw new Error("Can't access controller, AppAuth not built yet");
@@ -113,14 +126,8 @@ export class AppAuth extends Module {
action: AuthAction,
strategy: Strategy,
identifier: string,
- profile: ProfileExchange
+ profile: ProfileExchange,
): Promise {
- /*console.log("***** AppAuth:resolveUser", {
- action,
- strategy: strategy.getName(),
- identifier,
- profile
- });*/
if (!this.config.allow_register && action === "register") {
throw new Exception("Registration is not allowed", 403);
}
@@ -129,7 +136,7 @@ export class AppAuth extends Module {
.getFillableFields("create")
.map((f) => f.name);
const filteredProfile = Object.fromEntries(
- Object.entries(profile).filter(([key]) => fields.includes(key))
+ Object.entries(profile).filter(([key]) => fields.includes(key)),
);
switch (action) {
@@ -141,21 +148,10 @@ export class AppAuth extends Module {
}
private filterUserData(user: any) {
- /*console.log(
- "--filterUserData",
- user,
- this.config.jwt.fields,
- pick(user, this.config.jwt.fields)
- );*/
return pick(user, this.config.jwt.fields);
}
private async login(strategy: Strategy, identifier: string, profile: ProfileExchange) {
- /*console.log("--- trying to login", {
- strategy: strategy.getName(),
- identifier,
- profile
- });*/
if (!("email" in profile)) {
throw new Exception("Profile must have email");
}
@@ -172,18 +168,14 @@ export class AppAuth extends Module {
if (!result.data) {
throw new Exception("User not found", 404);
}
- //console.log("---login data", result.data, result);
// compare strategy and identifier
- //console.log("strategy comparison", result.data.strategy, strategy.getName());
if (result.data.strategy !== strategy.getName()) {
//console.log("!!! User registered with different strategy");
throw new Exception("User registered with different strategy");
}
- //console.log("identifier comparison", result.data.strategy_value, identifier);
if (result.data.strategy_value !== identifier) {
- //console.log("!!! Invalid credentials");
throw new Exception("Invalid credentials");
}
@@ -207,7 +199,7 @@ export class AppAuth extends Module {
const payload: any = {
...profile,
strategy: strategy.getName(),
- strategy_value: identifier
+ strategy_value: identifier,
};
const mutator = this.em.mutator(users);
@@ -257,13 +249,13 @@ export class AppAuth extends Module {
email: text().required(),
strategy: text({
fillable: ["create"],
- hidden: ["update", "form"]
+ hidden: ["update", "form"],
}).required(),
strategy_value: text({
fillable: ["create"],
- hidden: ["read", "table", "update", "form"]
+ hidden: ["read", "table", "update", "form"],
}).required(),
- role: text()
+ role: text(),
};
registerEntities() {
@@ -271,12 +263,12 @@ export class AppAuth extends Module {
this.ensureSchema(
em(
{
- [users.name as "users"]: users
+ [users.name as "users"]: users,
},
({ index }, { users }) => {
index(users).on(["email"], true).on(["strategy"]).on(["strategy_value"]);
- }
- )
+ },
+ ),
);
try {
@@ -285,6 +277,7 @@ export class AppAuth extends Module {
} catch (e) {}
try {
+ // also keep disabled strategies as a choice
const strategies = Object.keys(this.config.strategies ?? {});
this.replaceEntityField(users, "strategy", enumm({ enum: strategies }));
} catch (e) {}
@@ -304,7 +297,7 @@ export class AppAuth extends Module {
...(additional as any),
email,
strategy,
- strategy_value
+ strategy_value,
});
mutator.__unstable_toggleSystemEntityCreation(true);
return created;
@@ -315,9 +308,16 @@ export class AppAuth extends Module {
return this.configDefault;
}
+ const strategies = this.authenticator.getStrategies();
+
return {
...this.config,
- ...this.authenticator.toJSON(secrets)
+ ...this.authenticator.toJSON(secrets),
+ strategies: transformObject(strategies, (strategy) => ({
+ enabled: this.isStrategyEnabled(strategy),
+ type: strategy.getType(),
+ config: strategy.toJSON(secrets),
+ })),
};
}
}
diff --git a/app/src/auth/api/AuthApi.ts b/app/src/auth/api/AuthApi.ts
index f5ba882..d4cc3d2 100644
--- a/app/src/auth/api/AuthApi.ts
+++ b/app/src/auth/api/AuthApi.ts
@@ -10,13 +10,13 @@ export type AuthApiOptions = BaseModuleApiOptions & {
export class AuthApi extends ModuleApi {
protected override getDefaultOptions(): Partial {
return {
- basepath: "/api/auth"
+ basepath: "/api/auth",
};
}
async login(strategy: string, input: any) {
const res = await this.post([strategy, "login"], input, {
- credentials: "include"
+ credentials: "include",
});
if (res.ok && res.body.token) {
@@ -27,7 +27,7 @@ export class AuthApi extends ModuleApi {
async register(strategy: string, input: any) {
const res = await this.post([strategy, "register"], input, {
- credentials: "include"
+ credentials: "include",
});
if (res.ok && res.body.token) {
diff --git a/app/src/auth/api/AuthController.ts b/app/src/auth/api/AuthController.ts
index 3687395..536f93b 100644
--- a/app/src/auth/api/AuthController.ts
+++ b/app/src/auth/api/AuthController.ts
@@ -1,9 +1,9 @@
import { type AppAuth, AuthPermissions, type SafeUser, type Strategy } from "auth";
-import { TypeInvalidError, parse } from "core/utils";
+import { tbValidator as tb } from "core";
+import { Type, TypeInvalidError, parse, transformObject } from "core/utils";
import { DataPermissions } from "data";
import type { Hono } from "hono";
-import { Controller } from "modules/Controller";
-import type { ServerEnv } from "modules/Module";
+import { Controller, type ServerEnv } from "modules/Controller";
export type AuthActionResponse = {
success: boolean;
@@ -12,6 +12,10 @@ export type AuthActionResponse = {
errors?: any;
};
+const booleanLike = Type.Transform(Type.String())
+ .Decode((v) => v === "1")
+ .Encode((v) => (v ? "1" : "0"));
+
export class AuthController extends Controller {
constructor(private auth: AppAuth) {
super();
@@ -31,6 +35,9 @@ export class AuthController extends Controller {
}
private registerStrategyActions(strategy: Strategy, mainHono: Hono) {
+ if (!this.auth.isStrategyEnabled(strategy)) {
+ return;
+ }
const actions = strategy.getActions?.();
if (!actions) {
return;
@@ -51,7 +58,7 @@ export class AuthController extends Controller {
try {
const body = await this.auth.authenticator.getBody(c);
const valid = parse(create.schema, body, {
- skipMark: true
+ skipMark: true,
});
const processed = (await create.preprocess?.(valid)) ?? valid;
@@ -60,7 +67,7 @@ export class AuthController extends Controller {
mutator.__unstable_toggleSystemEntityCreation(false);
const { data: created } = await mutator.insertOne({
...processed,
- strategy: name
+ strategy: name,
});
mutator.__unstable_toggleSystemEntityCreation(true);
@@ -68,21 +75,21 @@ export class AuthController extends Controller {
success: true,
action: "create",
strategy: name,
- data: created as unknown as SafeUser
+ data: created as unknown as SafeUser,
} as AuthActionResponse);
} catch (e) {
if (e instanceof TypeInvalidError) {
return c.json(
{
success: false,
- errors: e.errors
+ errors: e.errors,
},
- 400
+ 400,
);
}
throw e;
}
- }
+ },
);
hono.get("create/schema.json", async (c) => {
return c.json(create.schema);
@@ -98,7 +105,8 @@ export class AuthController extends Controller {
const strategies = this.auth.authenticator.getStrategies();
for (const [name, strategy] of Object.entries(strategies)) {
- //console.log("registering", name, "at", `/${name}`);
+ if (!this.auth.isStrategyEnabled(strategy)) continue;
+
hono.route(`/${name}`, strategy.getController(this.auth.authenticator));
this.registerStrategyActions(strategy, hono);
}
@@ -127,10 +135,25 @@ export class AuthController extends Controller {
return c.redirect("/");
});
- hono.get("/strategies", async (c) => {
- const { strategies, basepath } = this.auth.toJSON(false);
- return c.json({ strategies, basepath });
- });
+ hono.get(
+ "/strategies",
+ tb("query", Type.Object({ include_disabled: Type.Optional(booleanLike) })),
+ async (c) => {
+ const { include_disabled } = c.req.valid("query");
+ const { strategies, basepath } = this.auth.toJSON(false);
+
+ if (!include_disabled) {
+ return c.json({
+ strategies: transformObject(strategies ?? {}, (strategy, name) => {
+ return this.auth.isStrategyEnabled(name) ? strategy : undefined;
+ }),
+ basepath,
+ });
+ }
+
+ return c.json({ strategies, basepath });
+ },
+ );
return hono.all("*", (c) => c.notFound());
}
diff --git a/app/src/auth/auth-schema.ts b/app/src/auth/auth-schema.ts
index 84882b5..ce45ea5 100644
--- a/app/src/auth/auth-schema.ts
+++ b/app/src/auth/auth-schema.ts
@@ -5,29 +5,30 @@ import { type Static, StringRecord, Type, objectTransform } from "core/utils";
export const Strategies = {
password: {
cls: PasswordStrategy,
- schema: PasswordStrategy.prototype.getSchema()
+ schema: PasswordStrategy.prototype.getSchema(),
},
oauth: {
cls: OAuthStrategy,
- schema: OAuthStrategy.prototype.getSchema()
+ schema: OAuthStrategy.prototype.getSchema(),
},
custom_oauth: {
cls: CustomOAuthStrategy,
- schema: CustomOAuthStrategy.prototype.getSchema()
- }
+ schema: CustomOAuthStrategy.prototype.getSchema(),
+ },
} as const;
export const STRATEGIES = Strategies;
const strategiesSchemaObject = objectTransform(STRATEGIES, (strategy, name) => {
return Type.Object(
{
+ enabled: Type.Optional(Type.Boolean({ default: true })),
type: Type.Const(name, { default: name, readOnly: true }),
- config: strategy.schema
+ config: strategy.schema,
},
{
title: name,
- additionalProperties: false
- }
+ additionalProperties: false,
+ },
);
});
const strategiesSchema = Type.Union(Object.values(strategiesSchemaObject));
@@ -36,15 +37,15 @@ export type AppAuthOAuthStrategy = Static;
export type AppAuthCustomOAuthStrategy = Static;
const guardConfigSchema = Type.Object({
- enabled: Type.Optional(Type.Boolean({ default: false }))
+ enabled: Type.Optional(Type.Boolean({ default: false })),
});
export const guardRoleSchema = Type.Object(
{
permissions: Type.Optional(Type.Array(Type.String())),
is_default: Type.Optional(Type.Boolean()),
- implicit_allow: Type.Optional(Type.Boolean())
+ implicit_allow: Type.Optional(Type.Boolean()),
},
- { additionalProperties: false }
+ { additionalProperties: false },
);
export const authConfigSchema = Type.Object(
@@ -61,20 +62,21 @@ export const authConfigSchema = Type.Object(
default: {
password: {
type: "password",
+ enabled: true,
config: {
- hashing: "sha256"
- }
- }
- }
- })
+ hashing: "sha256",
+ },
+ },
+ },
+ }),
),
guard: Type.Optional(guardConfigSchema),
- roles: Type.Optional(StringRecord(guardRoleSchema, { default: {} }))
+ roles: Type.Optional(StringRecord(guardRoleSchema, { default: {} })),
},
{
title: "Authentication",
- additionalProperties: false
- }
+ additionalProperties: false,
+ },
);
export type AppAuthSchema = Static;
diff --git a/app/src/auth/authenticate/Authenticator.ts b/app/src/auth/authenticate/Authenticator.ts
index f869318..6d1acc5 100644
--- a/app/src/auth/authenticate/Authenticator.ts
+++ b/app/src/auth/authenticate/Authenticator.ts
@@ -7,13 +7,13 @@ import {
Type,
parse,
runtimeSupports,
- transformObject
+ transformObject,
} from "core/utils";
import type { Context, Hono } from "hono";
import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie";
import { sign, verify } from "hono/jwt";
import type { CookieOptions } from "hono/utils/cookie";
-import type { ServerEnv } from "modules/Module";
+import type { ServerEnv } from "modules/Controller";
type Input = any; // workaround
export type JWTPayload = Parameters[0];
@@ -71,9 +71,9 @@ export const cookieConfig = Type.Partial(
expires: Type.Number({ default: defaultCookieExpires }), // seconds
renew: Type.Boolean({ default: true }),
pathSuccess: Type.String({ default: "/" }),
- pathLoggedOut: Type.String({ default: "/" })
+ pathLoggedOut: Type.String({ default: "/" }),
}),
- { default: {}, additionalProperties: false }
+ { default: {}, additionalProperties: false },
);
// @todo: maybe add a config to not allow cookie/api tokens to be used interchangably?
@@ -86,16 +86,16 @@ export const jwtConfig = Type.Object(
alg: Type.Optional(StringEnum(["HS256", "HS384", "HS512"], { default: "HS256" })),
expires: Type.Optional(Type.Number()), // seconds
issuer: Type.Optional(Type.String()),
- fields: Type.Array(Type.String(), { default: ["id", "email", "role"] })
+ fields: Type.Array(Type.String(), { default: ["id", "email", "role"] }),
},
{
default: {},
- additionalProperties: false
- }
+ additionalProperties: false,
+ },
);
export const authenticatorConfig = Type.Object({
jwt: jwtConfig,
- cookie: cookieConfig
+ cookie: cookieConfig,
});
type AuthConfig = Static;
@@ -104,7 +104,7 @@ export type AuthUserResolver = (
action: AuthAction,
strategy: Strategy,
identifier: string,
- profile: ProfileExchange
+ profile: ProfileExchange,
) => Promise;
type AuthClaims = SafeUser & {
iat: number;
@@ -127,7 +127,7 @@ export class Authenticator = Record<
action: AuthAction,
strategy: Strategy,
identifier: string,
- profile: ProfileExchange
+ profile: ProfileExchange,
): Promise {
//console.log("resolve", { action, strategy: strategy.getName(), profile });
const user = await this.userResolver(action, strategy, identifier, profile);
@@ -135,7 +135,7 @@ export class Authenticator = Record<
if (user) {
return {
user,
- token: await this.jwt(user)
+ token: await this.jwt(user),
};
}
@@ -148,7 +148,7 @@ export class Authenticator = Record<
strategy<
StrategyName extends keyof Strategies,
- Strat extends Strategy = Strategies[StrategyName]
+ Strat extends Strategy = Strategies[StrategyName],
>(strategy: StrategyName): Strat {
try {
return this.strategies[strategy] as unknown as Strat;
@@ -168,7 +168,7 @@ export class Authenticator = Record<
const payload: JWTPayload = {
...user,
- iat: Math.floor(Date.now() / 1000)
+ iat: Math.floor(Date.now() / 1000),
};
// issuer
@@ -194,7 +194,7 @@ export class Authenticator = Record<
const payload = await verify(
jwt,
this.config.jwt?.secret ?? "",
- this.config.jwt?.alg ?? "HS256"
+ this.config.jwt?.alg ?? "HS256",
);
// manually verify issuer (hono doesn't support it)
@@ -215,7 +215,7 @@ export class Authenticator = Record<
return {
...cookieConfig,
- expires: new Date(Date.now() + expires * 1000)
+ expires: new Date(Date.now() + expires * 1000),
};
}
@@ -343,17 +343,16 @@ export class Authenticator = Record<
return {
...this.config,
jwt: secrets ? this.config.jwt : undefined,
- strategies: transformObject(this.getStrategies(), (s) => s.toJSON(secrets))
};
}
}
export function createStrategyAction(
schema: S,
- preprocess: (input: Static) => Promise>
+ preprocess: (input: Static) => Promise>,
) {
return {
schema,
- preprocess
+ preprocess,
} as StrategyAction;
}
diff --git a/app/src/auth/authenticate/strategies/PasswordStrategy.ts b/app/src/auth/authenticate/strategies/PasswordStrategy.ts
index c6a9a37..8f30c58 100644
--- a/app/src/auth/authenticate/strategies/PasswordStrategy.ts
+++ b/app/src/auth/authenticate/strategies/PasswordStrategy.ts
@@ -9,7 +9,7 @@ type LoginSchema = { username: string; password: string } | { email: string; pas
type RegisterSchema = { email: string; password: string; [key: string]: any };
const schema = Type.Object({
- hashing: StringEnum(["plain", "sha256" /*, "bcrypt"*/] as const, { default: "sha256" })
+ hashing: StringEnum(["plain", "sha256" /*, "bcrypt"*/] as const, { default: "sha256" }),
});
export type PasswordStrategyOptions = Static;
@@ -49,7 +49,7 @@ export class PasswordStrategy implements Strategy {
return {
...input,
- password: await this.hash(input.password)
+ password: await this.hash(input.password),
};
}
@@ -62,8 +62,8 @@ export class PasswordStrategy implements Strategy {
tb(
"query",
Type.Object({
- redirect: Type.Optional(Type.String())
- })
+ redirect: Type.Optional(Type.String()),
+ }),
),
async (c) => {
const body = await authenticator.getBody(c);
@@ -75,22 +75,22 @@ export class PasswordStrategy implements Strategy {
"login",
this,
payload.password,
- payload
+ payload,
);
return await authenticator.respond(c, data, redirect);
} catch (e) {
return await authenticator.respond(c, e);
}
- }
+ },
)
.post(
"/register",
tb(
"query",
Type.Object({
- redirect: Type.Optional(Type.String())
- })
+ redirect: Type.Optional(Type.String()),
+ }),
),
async (c) => {
const body = await authenticator.getBody(c);
@@ -101,11 +101,11 @@ export class PasswordStrategy implements Strategy {
"register",
this,
payload.password,
- payload
+ payload,
);
return await authenticator.respond(c, data, redirect);
- }
+ },
);
}
@@ -114,19 +114,19 @@ export class PasswordStrategy implements Strategy {
create: createStrategyAction(
Type.Object({
email: Type.String({
- pattern: "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$"
+ pattern: "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$",
}),
password: Type.String({
- minLength: 8 // @todo: this should be configurable
- })
+ minLength: 8, // @todo: this should be configurable
+ }),
}),
async ({ password, ...input }) => {
return {
...input,
- strategy_value: await this.hash(password)
+ strategy_value: await this.hash(password),
};
- }
- )
+ },
+ ),
};
}
@@ -147,9 +147,6 @@ export class PasswordStrategy implements Strategy {
}
toJSON(secrets?: boolean) {
- return {
- type: this.getType(),
- config: secrets ? this.options : undefined
- };
+ return secrets ? this.options : undefined;
}
}
diff --git a/app/src/auth/authenticate/strategies/index.ts b/app/src/auth/authenticate/strategies/index.ts
index 97ea311..86c3ba2 100644
--- a/app/src/auth/authenticate/strategies/index.ts
+++ b/app/src/auth/authenticate/strategies/index.ts
@@ -9,5 +9,5 @@ export {
type PasswordStrategyOptions,
OAuthStrategy,
OAuthCallbackException,
- CustomOAuthStrategy
+ CustomOAuthStrategy,
};
diff --git a/app/src/auth/authenticate/strategies/oauth/CustomOAuthStrategy.ts b/app/src/auth/authenticate/strategies/oauth/CustomOAuthStrategy.ts
index 2ff54c9..6e7f458 100644
--- a/app/src/auth/authenticate/strategies/oauth/CustomOAuthStrategy.ts
+++ b/app/src/auth/authenticate/strategies/oauth/CustomOAuthStrategy.ts
@@ -15,11 +15,11 @@ const oauthSchemaCustom = Type.Object(
{
client_id: Type.String(),
client_secret: Type.String(),
- token_endpoint_auth_method: StringEnum(["client_secret_basic"])
+ token_endpoint_auth_method: StringEnum(["client_secret_basic"]),
},
{
- additionalProperties: false
- }
+ additionalProperties: false,
+ },
),
as: Type.Object(
{
@@ -29,15 +29,15 @@ const oauthSchemaCustom = Type.Object(
scope_separator: Type.Optional(Type.String({ default: " " })),
authorization_endpoint: Type.Optional(UrlString),
token_endpoint: Type.Optional(UrlString),
- userinfo_endpoint: Type.Optional(UrlString)
+ userinfo_endpoint: Type.Optional(UrlString),
},
{
- additionalProperties: false
- }
- )
+ additionalProperties: false,
+ },
+ ),
// @todo: profile mapping
},
- { title: "Custom OAuth", additionalProperties: false }
+ { title: "Custom OAuth", additionalProperties: false },
);
type OAuthConfigCustom = Static;
@@ -57,7 +57,7 @@ export type IssuerConfig = {
profile: (
info: UserInfo,
config: Omit,
- tokenResponse: any
+ tokenResponse: any,
) => Promise;
};
diff --git a/app/src/auth/authenticate/strategies/oauth/OAuthStrategy.ts b/app/src/auth/authenticate/strategies/oauth/OAuthStrategy.ts
index 6015ebd..b3177b5 100644
--- a/app/src/auth/authenticate/strategies/oauth/OAuthStrategy.ts
+++ b/app/src/auth/authenticate/strategies/oauth/OAuthStrategy.ts
@@ -18,14 +18,14 @@ const schemaProvided = Type.Object(
client: Type.Object(
{
client_id: Type.String(),
- client_secret: Type.String()
+ client_secret: Type.String(),
},
{
- additionalProperties: false
- }
- )
+ additionalProperties: false,
+ },
+ ),
},
- { title: "OAuth" }
+ { title: "OAuth" },
);
type ProvidedOAuthConfig = Static;
@@ -56,7 +56,7 @@ export type IssuerConfig = {
profile: (
info: UserInfo,
config: Omit,
- tokenResponse: any
+ tokenResponse: any,
) => Promise;
};
@@ -65,7 +65,7 @@ export class OAuthCallbackException extends Exception {
constructor(
public error: any,
- public step: string
+ public step: string,
) {
super("OAuthCallbackException on " + step);
}
@@ -103,8 +103,8 @@ export class OAuthStrategy implements Strategy {
type: info.type,
client: {
...info.client,
- ...this._config.client
- }
+ ...this._config.client,
+ },
};
}
@@ -129,7 +129,7 @@ export class OAuthStrategy implements Strategy {
const { challenge_supported, challenge, challenge_method } = await this.getCodeChallenge(
as,
- options.state
+ options.state,
);
if (!as.authorization_endpoint) {
@@ -150,7 +150,7 @@ export class OAuthStrategy implements Strategy {
client_id: client.client_id,
redirect_uri: options.redirect_uri,
response_type: "code",
- scope: scopes.join(as.scope_separator ?? " ")
+ scope: scopes.join(as.scope_separator ?? " "),
};
if (challenge_supported) {
params.code_challenge = challenge;
@@ -162,13 +162,13 @@ export class OAuthStrategy implements Strategy {
return {
url: new URL(endpoint) + "?" + new URLSearchParams(params).toString(),
endpoint,
- params
+ params,
};
}
private async oidc(
callbackParams: URL | URLSearchParams,
- options: { redirect_uri: string; state: string; scopes?: string[] }
+ options: { redirect_uri: string; state: string; scopes?: string[] },
) {
const config = await this.getConfig();
const { client, as, type } = config;
@@ -178,7 +178,7 @@ export class OAuthStrategy implements Strategy {
as,
client, // no client_secret required
callbackParams,
- oauth.expectNoState
+ oauth.expectNoState,
);
if (oauth.isOAuth2Error(parameters)) {
//console.log("callback.error", parameters);
@@ -193,7 +193,7 @@ export class OAuthStrategy implements Strategy {
client,
parameters,
options.redirect_uri,
- options.state
+ options.state,
);
//console.log("callback.response", response);
@@ -213,7 +213,7 @@ export class OAuthStrategy implements Strategy {
as,
client,
response,
- expectedNonce
+ expectedNonce,
);
if (oauth.isOAuth2Error(result)) {
console.log("callback.error", result);
@@ -236,7 +236,7 @@ export class OAuthStrategy implements Strategy {
private async oauth2(
callbackParams: URL | URLSearchParams,
- options: { redirect_uri: string; state: string; scopes?: string[] }
+ options: { redirect_uri: string; state: string; scopes?: string[] },
) {
const config = await this.getConfig();
const { client, type, as, profile } = config;
@@ -246,7 +246,7 @@ export class OAuthStrategy implements Strategy {
as,
client, // no client_secret required
callbackParams,
- oauth.expectNoState
+ oauth.expectNoState,
);
if (oauth.isOAuth2Error(parameters)) {
console.log("callback.error", parameters);
@@ -254,14 +254,14 @@ export class OAuthStrategy implements Strategy {
}
console.log(
"callback.parameters",
- JSON.stringify(Object.fromEntries(parameters.entries()), null, 2)
+ JSON.stringify(Object.fromEntries(parameters.entries()), null, 2),
);
const response = await oauth.authorizationCodeGrantRequest(
as,
client,
parameters,
options.redirect_uri,
- options.state
+ options.state,
);
const challenges = oauth.parseWwwAuthenticateChallenges(response);
@@ -297,7 +297,7 @@ export class OAuthStrategy implements Strategy {
async callback(
callbackParams: URL | URLSearchParams,
- options: { redirect_uri: string; state: string; scopes?: string[] }
+ options: { redirect_uri: string; state: string; scopes?: string[] },
): Promise {
const type = this.getIssuerConfig().type;
@@ -330,7 +330,7 @@ export class OAuthStrategy implements Strategy {
secure: true,
httpOnly: true,
sameSite: "Lax",
- maxAge: 60 * 5 // 5 minutes
+ maxAge: 60 * 5, // 5 minutes
});
};
@@ -339,7 +339,7 @@ export class OAuthStrategy implements Strategy {
return {
state: c.req.header("X-State-Challenge"),
action: c.req.header("X-State-Action"),
- mode: "token"
+ mode: "token",
} as any;
}
@@ -366,7 +366,7 @@ export class OAuthStrategy implements Strategy {
const profile = await this.callback(params, {
redirect_uri,
- state: state.state
+ state: state.state,
});
try {
@@ -392,7 +392,7 @@ export class OAuthStrategy implements Strategy {
const params = new URLSearchParams(url.search);
return c.json({
- code: params.get("code") ?? null
+ code: params.get("code") ?? null,
});
});
@@ -410,7 +410,7 @@ export class OAuthStrategy implements Strategy {
const state = oauth.generateRandomCodeVerifier();
const response = await this.request({
redirect_uri,
- state
+ state,
});
//console.log("_state", state);
@@ -433,7 +433,7 @@ export class OAuthStrategy implements Strategy {
const state = oauth.generateRandomCodeVerifier();
const response = await this.request({
redirect_uri,
- state
+ state,
});
if (isDebug()) {
@@ -442,14 +442,14 @@ export class OAuthStrategy implements Strategy {
redirect_uri,
challenge: state,
action,
- params: response.params
+ params: response.params,
});
}
return c.json({
url: response.url,
challenge: state,
- action
+ action,
});
});
@@ -476,11 +476,8 @@ export class OAuthStrategy implements Strategy {
const config = secrets ? this.config : filterKeys(this.config, ["secret", "client_id"]);
return {
- type: this.getType(),
- config: {
- type: this.getIssuerConfig().type,
- ...config
- }
+ type: this.getIssuerConfig().type,
+ ...config,
};
}
}
diff --git a/app/src/auth/authorize/Guard.ts b/app/src/auth/authorize/Guard.ts
index 8fbeadd..8a1da25 100644
--- a/app/src/auth/authorize/Guard.ts
+++ b/app/src/auth/authorize/Guard.ts
@@ -1,7 +1,7 @@
import { Exception, Permission } from "core";
import { objectTransform } from "core/utils";
import type { Context } from "hono";
-import type { ServerEnv } from "modules/Module";
+import type { ServerEnv } from "modules/Controller";
import { Role } from "./Role";
export type GuardUserContext = {
@@ -37,7 +37,7 @@ export class Guard {
implicit_allow?: boolean;
}
>,
- config?: GuardConfig
+ config?: GuardConfig,
) {
const _roles = roles
? objectTransform(roles, ({ permissions = [], is_default, implicit_allow }, name) => {
@@ -103,7 +103,7 @@ export class Guard {
debug &&
console.log("guard: role not found", {
user: user,
- role: user?.role
+ role: user?.role,
});
return this.getDefaultRole();
}
@@ -141,14 +141,14 @@ export class Guard {
}
const rolePermission = role.permissions.find(
- (rolePermission) => rolePermission.permission.name === name
+ (rolePermission) => rolePermission.permission.name === name,
);
debug &&
console.log("guard: rolePermission, allowing?", {
permission: name,
role: role.name,
- allowing: !!rolePermission
+ allowing: !!rolePermission,
});
return !!rolePermission;
}
@@ -162,7 +162,7 @@ export class Guard {
if (!this.granted(permission, c)) {
throw new Exception(
`Permission "${typeof permission === "string" ? permission : permission.name}" not granted`,
- 403
+ 403,
);
}
}
diff --git a/app/src/auth/authorize/Role.ts b/app/src/auth/authorize/Role.ts
index 6d1e0c5..b5b09b2 100644
--- a/app/src/auth/authorize/Role.ts
+++ b/app/src/auth/authorize/Role.ts
@@ -3,7 +3,7 @@ import { Permission } from "core";
export class RolePermission {
constructor(
public permission: Permission,
- public config?: any
+ public config?: any,
) {}
}
@@ -12,20 +12,20 @@ export class Role {
public name: string,
public permissions: RolePermission[] = [],
public is_default: boolean = false,
- public implicit_allow: boolean = false
+ public implicit_allow: boolean = false,
) {}
static createWithPermissionNames(
name: string,
permissionNames: string[],
is_default: boolean = false,
- implicit_allow: boolean = false
+ implicit_allow: boolean = false,
) {
return new Role(
name,
permissionNames.map((name) => new RolePermission(new Permission(name))),
is_default,
- implicit_allow
+ implicit_allow,
);
}
@@ -39,7 +39,7 @@ export class Role {
config.name,
config.permissions?.map((name) => new RolePermission(new Permission(name))) ?? [],
config.is_default,
- config.implicit_allow
+ config.implicit_allow,
);
}
}
diff --git a/app/src/auth/index.ts b/app/src/auth/index.ts
index 11c3367..b99de5a 100644
--- a/app/src/auth/index.ts
+++ b/app/src/auth/index.ts
@@ -12,7 +12,7 @@ export {
type AuthUserResolver,
Authenticator,
authenticatorConfig,
- jwtConfig
+ jwtConfig,
} from "./authenticate/Authenticator";
export { AppAuth, type UserFieldSchema } from "./AppAuth";
diff --git a/app/src/auth/middlewares.ts b/app/src/auth/middlewares.ts
index 677478b..83e64ce 100644
--- a/app/src/auth/middlewares.ts
+++ b/app/src/auth/middlewares.ts
@@ -2,7 +2,7 @@ import type { Permission } from "core";
import { patternMatch } from "core/utils";
import type { Context } from "hono";
import { createMiddleware } from "hono/factory";
-import type { ServerEnv } from "modules/Module";
+import type { ServerEnv } from "modules/Controller";
function getPath(reqOrCtx: Request | Context) {
const req = reqOrCtx instanceof Request ? reqOrCtx : reqOrCtx.req.raw;
@@ -36,7 +36,7 @@ export const auth = (options?: {
registered: false,
resolved: false,
skip: false,
- user: undefined
+ user: undefined,
});
}
@@ -77,7 +77,7 @@ export const permission = (
options?: {
onGranted?: (c: Context) => Promise;
onDenied?: (c: Context) => Promise;
- }
+ },
) =>
// @ts-ignore
createMiddleware(async (c, next) => {
diff --git a/app/src/cli/commands/copy-assets.ts b/app/src/cli/commands/copy-assets.ts
new file mode 100644
index 0000000..813288c
--- /dev/null
+++ b/app/src/cli/commands/copy-assets.ts
@@ -0,0 +1,36 @@
+import { getRelativeDistPath } from "cli/utils/sys";
+import type { CliCommand } from "../types";
+import { Option } from "commander";
+import fs from "node:fs/promises";
+import path from "node:path";
+import c from "picocolors";
+
+export const copyAssets: CliCommand = (program) => {
+ program
+ .command("copy-assets")
+ .description("copy static assets")
+ .addOption(new Option("-o --out ", "directory to copy to"))
+ .addOption(new Option("-c --clean", "clean the output directory"))
+ .action(action);
+};
+
+async function action(options: { out?: string; clean?: boolean }) {
+ const out = options.out ?? "static";
+
+ // clean "out" directory
+ if (options.clean) {
+ await fs.rm(out, { recursive: true, force: true });
+ }
+
+ // recursively copy from src/assets to out using node fs
+ const from = path.resolve(getRelativeDistPath(), "static");
+ await fs.cp(from, out, { recursive: true });
+
+ // in out, move ".vite/manifest.json" to "manifest.json"
+ await fs.rename(path.resolve(out, ".vite/manifest.json"), path.resolve(out, "manifest.json"));
+
+ // delete ".vite" directory in out
+ await fs.rm(path.resolve(out, ".vite"), { recursive: true });
+
+ console.log(c.green(`Assets copied to: ${c.bold(out)}`));
+}
diff --git a/app/src/cli/commands/create/create.ts b/app/src/cli/commands/create/create.ts
index 6d74385..f1f9a5e 100644
--- a/app/src/cli/commands/create/create.ts
+++ b/app/src/cli/commands/create/create.ts
@@ -5,7 +5,7 @@ import type { CliCommand } from "cli/types";
import { typewriter, wait } from "cli/utils/cli";
import { execAsync, getVersion } from "cli/utils/sys";
import { Option } from "commander";
-import { colorizeConsole } from "core";
+import { env } from "core";
import color from "picocolors";
import { overridePackageJson, updateBkndPackages } from "./npm";
import { type Template, templates } from "./templates";
@@ -13,18 +13,19 @@ import { type Template, templates } from "./templates";
const config = {
types: {
runtime: "Runtime",
- framework: "Framework"
+ framework: "Framework",
},
runtime: {
node: "Node.js",
bun: "Bun",
- cloudflare: "Cloudflare"
+ cloudflare: "Cloudflare",
+ aws: "AWS Lambda",
},
framework: {
nextjs: "Next.js",
remix: "Remix",
- astro: "Astro"
- }
+ astro: "Astro",
+ },
} as const;
export const create: CliCommand = (program) => {
@@ -41,7 +42,7 @@ function errorOutro() {
$p.outro(color.red("Failed to create project."));
console.log(
color.yellow("Sorry that this happened. If you think this is a bug, please report it at: ") +
- color.cyan("https://github.com/bknd-io/bknd/issues")
+ color.cyan("https://github.com/bknd-io/bknd/issues"),
);
console.log("");
process.exit(1);
@@ -49,30 +50,29 @@ function errorOutro() {
async function action(options: { template?: string; dir?: string; integration?: string }) {
console.log("");
- colorizeConsole(console);
const downloadOpts = {
dir: options.dir || "./",
- clean: false
+ clean: false,
};
const version = await getVersion();
$p.intro(
- `👋 Welcome to the ${color.bold(color.cyan("bknd"))} create wizard ${color.bold(`v${version}`)}`
+ `👋 Welcome to the ${color.bold(color.cyan("bknd"))} create cli ${color.bold(`v${version}`)}`,
);
await $p.stream.message(
(async function* () {
yield* typewriter("Thanks for choosing to create a new project with bknd!", color.dim);
await wait();
- })()
+ })(),
);
if (!options.dir) {
const dir = await $p.text({
message: "Where to create your project?",
placeholder: downloadOpts.dir,
- initialValue: downloadOpts.dir
+ initialValue: downloadOpts.dir,
});
if ($p.isCancel(dir)) {
process.exit(1);
@@ -84,7 +84,7 @@ async function action(options: { template?: string; dir?: string; integration?:
if (fs.existsSync(downloadOpts.dir)) {
const clean = await $p.confirm({
message: `Directory ${color.cyan(downloadOpts.dir)} exists. Clean it?`,
- initialValue: false
+ initialValue: false,
});
if ($p.isCancel(clean)) {
process.exit(1);
@@ -115,7 +115,7 @@ async function action(options: { template?: string; dir?: string; integration?:
await wait(2);
yield* typewriter("Let's find the perfect template for you.", color.dim);
await wait(2);
- })()
+ })(),
);
const type = await $p.select({
@@ -123,8 +123,8 @@ async function action(options: { template?: string; dir?: string; integration?:
options: Object.entries(config.types).map(([value, name]) => ({
value,
label: name,
- hint: Object.values(config[value]).join(", ")
- }))
+ hint: Object.values(config[value]).join(", "),
+ })),
});
if ($p.isCancel(type)) {
@@ -135,8 +135,8 @@ async function action(options: { template?: string; dir?: string; integration?:
message: `Which ${color.cyan(config.types[type])} do you want to continue with?`,
options: Object.entries(config[type]).map(([value, name]) => ({
value,
- label: name
- })) as any
+ label: name,
+ })) as any,
});
if ($p.isCancel(_integration)) {
process.exit(1);
@@ -157,7 +157,7 @@ async function action(options: { template?: string; dir?: string; integration?:
} else if (choices.length > 1) {
const selected_template = await $p.select({
message: "Pick a template",
- options: choices.map((t) => ({ value: t.key, label: t.title, hint: t.description }))
+ options: choices.map((t) => ({ value: t.key, label: t.title, hint: t.description })),
});
if ($p.isCancel(selected_template)) {
@@ -177,10 +177,11 @@ async function action(options: { template?: string; dir?: string; integration?:
const ctx = { template, dir: downloadOpts.dir, name };
{
- const ref = process.env.BKND_CLI_CREATE_REF ?? `v${version}`;
- if (process.env.BKND_CLI_CREATE_REF) {
- $p.log.warn(color.dim("[DEV] Using local ref: ") + color.yellow(ref));
- }
+ const ref = env("cli_create_ref", `#v${version}`, {
+ onValid: (given) => {
+ $p.log.warn(color.dim("[DEV] Using local ref: ") + color.yellow(given));
+ },
+ });
const prefix =
template.ref === true
@@ -196,7 +197,7 @@ async function action(options: { template?: string; dir?: string; integration?:
try {
await downloadTemplate(url, {
dir: ctx.dir,
- force: downloadOpts.clean ? "clean" : true
+ force: downloadOpts.clean ? "clean" : true,
});
} catch (e) {
if (e instanceof Error) {
@@ -221,15 +222,15 @@ async function action(options: { template?: string; dir?: string; integration?:
await overridePackageJson(
(pkg) => ({
...pkg,
- name: ctx.name
+ name: ctx.name,
}),
- { dir: ctx.dir }
+ { dir: ctx.dir },
);
$p.log.success(`Updated package name to ${color.cyan(ctx.name)}`);
{
const install = await $p.confirm({
- message: "Install dependencies?"
+ message: "Install dependencies?",
});
if ($p.isCancel(install)) {
@@ -263,10 +264,10 @@ async function action(options: { template?: string; dir?: string; integration?:
yield* typewriter(
color.dim("Remember to run ") +
color.cyan("npm install") +
- color.dim(" after setup")
+ color.dim(" after setup"),
);
await wait();
- })()
+ })(),
);
}
}
@@ -284,10 +285,10 @@ async function action(options: { template?: string; dir?: string; integration?:
yield "\n\n";
yield* typewriter(
`Enter your project's directory using ${color.cyan("cd " + ctx.dir)}
-If you need help, check ${color.cyan("https://docs.bknd.io")} or join our Discord!`
+If you need help, check ${color.cyan("https://docs.bknd.io")} or join our Discord!`,
);
await wait(2);
- })()
+ })(),
);
$p.outro(color.green("Setup complete."));
diff --git a/app/src/cli/commands/create/npm.ts b/app/src/cli/commands/create/npm.ts
index 49fd2c0..690d891 100644
--- a/app/src/cli/commands/create/npm.ts
+++ b/app/src/cli/commands/create/npm.ts
@@ -16,7 +16,7 @@ export type TPackageJson = Partial<{
export async function overrideJson(
file: string,
fn: (pkg: File) => Promise | File,
- opts?: { dir?: string; indent?: number }
+ opts?: { dir?: string; indent?: number },
) {
const pkgPath = path.resolve(opts?.dir ?? process.cwd(), file);
const pkg = await readFile(pkgPath, "utf-8");
@@ -26,7 +26,7 @@ export async function overrideJson(
export async function overridePackageJson(
fn: (pkg: TPackageJson) => Promise | TPackageJson,
- opts?: { dir?: string }
+ opts?: { dir?: string },
) {
return await overrideJson("package.json", fn, { dir: opts?.dir });
}
@@ -44,7 +44,7 @@ export async function getVersion(pkg: string, version: string = "latest") {
const _deps = ["dependencies", "devDependencies", "optionalDependencies"] as const;
export async function replacePackageJsonVersions(
fn: (pkg: string, version: string) => Promise | string | undefined,
- opts?: { include?: (keyof typeof _deps)[]; dir?: string }
+ opts?: { include?: (keyof typeof _deps)[]; dir?: string },
) {
const deps = (opts?.include ?? _deps) as string[];
await overridePackageJson(
@@ -62,14 +62,14 @@ export async function replacePackageJsonVersions(
return json;
},
- { dir: opts?.dir }
+ { dir: opts?.dir },
);
}
export async function updateBkndPackages(dir?: string, map?: Record) {
const versions = {
- bknd: "^" + (await sysGetVersion()),
- ...(map ?? {})
+ bknd: await sysGetVersion(),
+ ...(map ?? {}),
};
await replacePackageJsonVersions(
async (pkg) => {
@@ -78,6 +78,6 @@ export async function updateBkndPackages(dir?: string, map?: Record")} and update your wrangler configuration.`,
- c.dim
+ c.dim,
);
- })()
+ })(),
);
}
@@ -108,10 +108,10 @@ async function createLibsql(ctx: TemplateSetupCtx) {
(json) => ({
...json,
vars: {
- DB_URL: "http://127.0.0.1:8080"
- }
+ DB_URL: "http://127.0.0.1:8080",
+ },
}),
- { dir: ctx.dir }
+ { dir: ctx.dir },
);
await overridePackageJson(
@@ -120,10 +120,10 @@ async function createLibsql(ctx: TemplateSetupCtx) {
scripts: {
...pkg.scripts,
db: "turso dev",
- dev: "npm run db && wrangler dev"
- }
+ dev: "npm run db && wrangler dev",
+ },
}),
- { dir: ctx.dir }
+ { dir: ctx.dir },
);
await $p.stream.info(
@@ -132,13 +132,13 @@ async function createLibsql(ctx: TemplateSetupCtx) {
await wait();
yield* typewriter(
`\nYou can now run ${c.cyan("npm run db")} to start the database and ${c.cyan("npm run dev")} to start the worker.`,
- c.dim
+ c.dim,
);
await wait();
yield* typewriter(
`\nAlso make sure you have Turso's CLI installed. Check their docs on how to install at ${c.cyan("https://docs.turso.tech/cli/introduction")}`,
- c.dim
+ c.dim,
);
- })()
+ })(),
);
}
diff --git a/app/src/cli/commands/create/templates/index.ts b/app/src/cli/commands/create/templates/index.ts
index 469827f..f3cf9b5 100644
--- a/app/src/cli/commands/create/templates/index.ts
+++ b/app/src/cli/commands/create/templates/index.ts
@@ -8,7 +8,15 @@ export type TemplateSetupCtx = {
name: string;
};
-export type Integration = "node" | "bun" | "cloudflare" | "nextjs" | "remix" | "astro" | "custom";
+export type Integration =
+ | "node"
+ | "bun"
+ | "cloudflare"
+ | "nextjs"
+ | "remix"
+ | "astro"
+ | "aws"
+ | "custom";
type TemplateScripts = "install" | "dev" | "build" | "start";
export type Template = {
@@ -43,7 +51,7 @@ export const templates: Template[] = [
integration: "node",
description: "A basic bknd Node.js server",
path: "gh:bknd-io/bknd/examples/node",
- ref: true
+ ref: true,
},
{
key: "bun",
@@ -51,7 +59,7 @@ export const templates: Template[] = [
integration: "bun",
description: "A basic bknd Bun server",
path: "gh:bknd-io/bknd/examples/bun",
- ref: true
+ ref: true,
},
{
key: "astro",
@@ -59,6 +67,14 @@ export const templates: Template[] = [
integration: "astro",
description: "A basic bknd Astro starter",
path: "gh:bknd-io/bknd/examples/astro",
- ref: true
- }
+ ref: true,
+ },
+ {
+ key: "aws",
+ title: "AWS Lambda Basic",
+ integration: "aws",
+ description: "A basic bknd AWS Lambda starter",
+ path: "gh:bknd-io/bknd/examples/aws-lambda",
+ ref: true,
+ },
];
diff --git a/app/src/cli/commands/create/templates/nextjs.ts b/app/src/cli/commands/create/templates/nextjs.ts
index bd36c2a..94b4b58 100644
--- a/app/src/cli/commands/create/templates/nextjs.ts
+++ b/app/src/cli/commands/create/templates/nextjs.ts
@@ -9,7 +9,7 @@ export const nextjs = {
description: "A basic bknd Next.js starter",
path: "gh:bknd-io/bknd/examples/nextjs",
scripts: {
- install: "npm install --force"
+ install: "npm install --force",
},
ref: true,
preinstall: async (ctx) => {
@@ -20,10 +20,10 @@ export const nextjs = {
dependencies: {
...pkg.dependencies,
react: undefined,
- "react-dom": undefined
- }
+ "react-dom": undefined,
+ },
}),
- { dir: ctx.dir }
+ { dir: ctx.dir },
);
- }
+ },
} as const satisfies Template;
diff --git a/app/src/cli/commands/create/templates/remix.ts b/app/src/cli/commands/create/templates/remix.ts
index 42d494e..3eef651 100644
--- a/app/src/cli/commands/create/templates/remix.ts
+++ b/app/src/cli/commands/create/templates/remix.ts
@@ -16,10 +16,10 @@ export const remix = {
dependencies: {
...pkg.dependencies,
react: "^18.2.0",
- "react-dom": "^18.2.0"
- }
+ "react-dom": "^18.2.0",
+ },
}),
- { dir: ctx.dir }
+ { dir: ctx.dir },
);
- }
+ },
} as const satisfies Template;
diff --git a/app/src/cli/commands/debug.ts b/app/src/cli/commands/debug.ts
index 167e0ed..9f817c3 100644
--- a/app/src/cli/commands/debug.ts
+++ b/app/src/cli/commands/debug.ts
@@ -23,7 +23,7 @@ const subjects = {
relativeDistPath: getRelativeDistPath(),
cwd: process.cwd(),
dir: path.dirname(url.fileURLToPath(import.meta.url)),
- resolvedPkg: path.resolve(getRootPath(), "package.json")
+ resolvedPkg: path.resolve(getRootPath(), "package.json"),
});
},
routes: async () => {
@@ -32,7 +32,7 @@ const subjects = {
const app = createApp({ connection: credentials });
await app.build();
showRoutes(app.server);
- }
+ },
};
async function action(subject: string) {
diff --git a/app/src/cli/commands/index.ts b/app/src/cli/commands/index.ts
index e27235f..cc1b2ae 100644
--- a/app/src/cli/commands/index.ts
+++ b/app/src/cli/commands/index.ts
@@ -4,3 +4,4 @@ export { run } from "./run";
export { debug } from "./debug";
export { user } from "./user";
export { create } from "./create";
+export { copyAssets } from "./copy-assets";
diff --git a/app/src/cli/commands/run/platform.ts b/app/src/cli/commands/run/platform.ts
index eda86e4..62707bd 100644
--- a/app/src/cli/commands/run/platform.ts
+++ b/app/src/cli/commands/run/platform.ts
@@ -14,13 +14,13 @@ export async function serveStatic(server: Platform): Promise
const m = await import("@hono/node-server/serve-static");
return m.serveStatic({
// somehow different for node
- root: getRelativeDistPath() + "/static"
+ root: getRelativeDistPath() + "/static",
});
}
case "bun": {
const m = await import("hono/bun");
return m.serveStatic({
- root: path.resolve(getRelativeDistPath(), "static")
+ root: path.resolve(getRelativeDistPath(), "static"),
});
}
}
@@ -30,7 +30,11 @@ export async function attachServeStatic(app: any, platform: Platform) {
app.module.server.client.get(config.server.assets_path + "*", await serveStatic(platform));
}
-export async function startServer(server: Platform, app: any, options: { port: number }) {
+export async function startServer(
+ server: Platform,
+ app: any,
+ options: { port: number; open?: boolean },
+) {
const port = options.port;
console.log(`Using ${server} serve`);
@@ -40,14 +44,14 @@ export async function startServer(server: Platform, app: any, options: { port: n
const serve = await import("@hono/node-server").then((m) => m.serve);
serve({
fetch: (req) => app.fetch(req),
- port
+ port,
});
break;
}
case "bun": {
Bun.serve({
fetch: (req) => app.fetch(req),
- port
+ port,
});
break;
}
@@ -55,7 +59,9 @@ export async function startServer(server: Platform, app: any, options: { port: n
const url = `http://localhost:${port}`;
console.info("Server listening on", url);
- await open(url);
+ if (options.open) {
+ await open(url);
+ }
}
export async function getConfigPath(filePath?: string) {
diff --git a/app/src/cli/commands/run/run.ts b/app/src/cli/commands/run/run.ts
index c4e5442..ddfeb1a 100644
--- a/app/src/cli/commands/run/run.ts
+++ b/app/src/cli/commands/run/run.ts
@@ -13,7 +13,7 @@ import {
attachServeStatic,
getConfigPath,
getConnectionCredentialsFromEnv,
- startServer
+ startServer,
} from "./platform";
dotenv.config();
@@ -26,27 +26,28 @@ export const run: CliCommand = (program) => {
new Option("-p, --port ", "port to run on")
.env("PORT")
.default(config.server.default_port)
- .argParser((v) => Number.parseInt(v))
+ .argParser((v) => Number.parseInt(v)),
)
.addOption(
new Option("-m, --memory", "use in-memory database").conflicts([
"config",
"db-url",
- "db-token"
- ])
+ "db-token",
+ ]),
)
.addOption(new Option("-c, --config ", "config file"))
.addOption(
new Option("--db-url ", "database url, can be any valid libsql url").conflicts(
- "config"
- )
+ "config",
+ ),
)
.addOption(new Option("--db-token ", "database token").conflicts("config"))
.addOption(
new Option("--server ", "server type")
.choices(PLATFORMS)
- .default(isBun ? "bun" : "node")
+ .default(isBun ? "bun" : "node"),
)
+ .addOption(new Option("--no-open", "don't open browser window on start"))
.action(action);
};
@@ -76,7 +77,7 @@ async function makeApp(config: MakeAppConfig) {
await config.onBuilt(app);
}
},
- "sync"
+ "sync",
);
await app.build();
@@ -95,7 +96,7 @@ export async function makeConfigApp(config: CliBkndConfig, platform?: Platform)
await config.onBuilt?.(app);
},
- "sync"
+ "sync",
);
await config.beforeBuild?.(app);
@@ -110,6 +111,7 @@ async function action(options: {
dbUrl?: string;
dbToken?: string;
server: Platform;
+ open?: boolean;
}) {
colorizeConsole(console);
const configFilePath = await getConfigPath(options.config);
@@ -141,9 +143,9 @@ async function action(options: {
console.info("Using connection", c.cyan(connection.url));
app = await makeApp({
connection,
- server: { platform: options.server }
+ server: { platform: options.server },
});
}
- await startServer(options.server, app, { port: options.port });
+ await startServer(options.server, app, { port: options.port, open: options.open });
}
diff --git a/app/src/cli/commands/user.ts b/app/src/cli/commands/user.ts
index 235fd4c..3545093 100644
--- a/app/src/cli/commands/user.ts
+++ b/app/src/cli/commands/user.ts
@@ -48,7 +48,7 @@ async function create(app: App, options: any) {
return "Invalid email";
}
return;
- }
+ },
});
const password = await $password({
@@ -58,7 +58,7 @@ async function create(app: App, options: any) {
return "Invalid password";
}
return;
- }
+ },
});
if (typeof email !== "string" || typeof password !== "string") {
@@ -69,8 +69,8 @@ async function create(app: App, options: any) {
try {
const created = await app.createUser({
email,
- password: await strategy.hash(password as string)
- })
+ password: await strategy.hash(password as string),
+ });
console.log("Created:", created);
} catch (e) {
console.error("Error", e);
@@ -90,7 +90,7 @@ async function update(app: App, options: any) {
return "Invalid email";
}
return;
- }
+ },
})) as string;
if (typeof email !== "string") {
console.log("Cancelled");
@@ -111,7 +111,7 @@ async function update(app: App, options: any) {
return "Invalid password";
}
return;
- }
+ },
});
if (typeof password !== "string") {
console.log("Cancelled");
@@ -130,7 +130,7 @@ async function update(app: App, options: any) {
.ctx()
.em.mutator(users_entity)
.updateOne(user.id, {
- strategy_value: await strategy.hash(password as string)
+ strategy_value: await strategy.hash(password as string),
});
togglePw(false);
@@ -138,4 +138,4 @@ async function update(app: App, options: any) {
} catch (e) {
console.error("Error", e);
}
-}
\ No newline at end of file
+}
diff --git a/app/src/cli/utils/cli.ts b/app/src/cli/utils/cli.ts
index 5b0d8e7..ab7c341 100644
--- a/app/src/cli/utils/cli.ts
+++ b/app/src/cli/utils/cli.ts
@@ -12,7 +12,7 @@ export default function ansiRegex({ onlyFirst = false } = {}) {
const ST = "(?:\\u0007|\\u001B\\u005C|\\u009C)";
const pattern = [
`[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?${ST})`,
- "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))"
+ "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))",
].join("|");
return new RegExp(pattern, onlyFirst ? undefined : "g");
@@ -22,7 +22,7 @@ const DEFAULT_WAIT_WRITER = _SPEEDUP ? 0 : 20;
export async function* typewriter(
text: string,
transform?: (char: string) => string,
- _delay?: number
+ _delay?: number,
) {
const delay = DEFAULT_WAIT_WRITER * (_delay ?? 1);
const regex = ansiRegex();
diff --git a/app/src/cli/utils/sys.ts b/app/src/cli/utils/sys.ts
index 4ea3531..ba6c579 100644
--- a/app/src/cli/utils/sys.ts
+++ b/app/src/cli/utils/sys.ts
@@ -44,7 +44,7 @@ export function exec(command: string, opts?: { silent?: boolean; env?: Record }
+ opts?: { silent?: boolean; env?: Record },
) {
return new Promise((resolve, reject) => {
nodeExec(
command,
{
- env: { ...process.env, ...opts?.env }
+ env: { ...process.env, ...opts?.env },
},
(err, stdout, stderr) => {
if (err) {
return reject(err);
}
resolve(stdout);
- }
+ },
);
});
}
diff --git a/app/src/core/cache/adapters/MemoryCache.ts b/app/src/core/cache/adapters/MemoryCache.ts
index 75e0df7..5cf36b2 100644
--- a/app/src/core/cache/adapters/MemoryCache.ts
+++ b/app/src/core/cache/adapters/MemoryCache.ts
@@ -10,7 +10,7 @@ export class MemoryCache implements ICachePool {
supports = () => ({
metadata: true,
- clear: true
+ clear: true,
});
async get(key: string): Promise> {
@@ -61,7 +61,7 @@ export class MemoryCache implements ICachePool {
async put(
key: string,
value: Data,
- options: { expiresAt?: Date; ttl?: number; metadata?: Record } = {}
+ options: { expiresAt?: Date; ttl?: number; metadata?: Record } = {},
): Promise {
const item = await this.get(key);
item.set(value, options.metadata || {});
diff --git a/app/src/core/config.ts b/app/src/core/config.ts
index 2f2cf06..f48385f 100644
--- a/app/src/core/config.ts
+++ b/app/src/core/config.ts
@@ -17,9 +17,9 @@ export const config = {
server: {
default_port: 1337,
// resetted to root for now, bc bundling with vite
- assets_path: "/"
+ assets_path: "/",
},
data: {
- default_primary_field: "id"
- }
+ default_primary_field: "id",
+ },
} as const;
diff --git a/app/src/core/console.ts b/app/src/core/console.ts
index 6bcb637..2d8e11b 100644
--- a/app/src/core/console.ts
+++ b/app/src/core/console.ts
@@ -1,4 +1,6 @@
+import { datetimeStringLocal } from "core/utils";
import colors from "picocolors";
+import { env } from "core";
function hasColors() {
try {
@@ -8,10 +10,10 @@ function hasColors() {
env = p.env || {};
return (
!(!!env.NO_COLOR || argv.includes("--no-color")) &&
- // biome-ignore lint/complexity/useOptionalChain:
(!!env.FORCE_COLOR ||
argv.includes("--color") ||
p.platform === "win32" ||
+ // biome-ignore lint/complexity/useOptionalChain:
((p.stdout || {}).isTTY && env.TERM !== "dumb") ||
!!env.CI)
);
@@ -20,86 +22,74 @@ function hasColors() {
}
}
-const originalConsoles = {
- error: console.error,
- warn: console.warn,
- info: console.info,
- log: console.log,
- debug: console.debug
-} as typeof console;
+const __consoles = {
+ error: {
+ prefix: "ERR",
+ color: colors.red,
+ args_color: colors.red,
+ original: console.error,
+ },
+ warn: {
+ prefix: "WRN",
+ color: colors.yellow,
+ args_color: colors.yellow,
+ original: console.warn,
+ },
+ info: {
+ prefix: "INF",
+ color: colors.cyan,
+ original: console.info,
+ },
+ log: {
+ prefix: "LOG",
+ color: colors.dim,
+ args_color: colors.dim,
+ original: console.log,
+ },
+ debug: {
+ prefix: "DBG",
+ color: colors.yellow,
+ args_color: colors.dim,
+ original: console.debug,
+ },
+} as const;
-function __tty(type: any, args: any[]) {
+function __tty(_type: any, args: any[]) {
const has = hasColors();
- const styles = {
- error: {
- prefix: colors.red,
- args: colors.red
- },
- warn: {
- prefix: colors.yellow,
- args: colors.yellow
- },
- info: {
- prefix: colors.cyan
- },
- log: {
- prefix: colors.gray
- },
- debug: {
- prefix: colors.yellow
- }
- } as const;
- const prefix = styles[type].prefix(
- `[${type.toUpperCase()}]${has ? " ".repeat(5 - type.length) : ""}`
- );
+ const cons = __consoles[_type];
+ const prefix = cons.color(`[${cons.prefix}]`);
const _args = args.map((a) =>
- "args" in styles[type] && has && typeof a === "string" ? styles[type].args(a) : a
+ "args_color" in cons && has && typeof a === "string" ? cons.args_color(a) : a,
);
- return originalConsoles[type](prefix, ..._args);
+ return cons.original(prefix, colors.gray(datetimeStringLocal()), ..._args);
}
-export type TConsoleSeverity = keyof typeof originalConsoles;
-const severities = Object.keys(originalConsoles) as TConsoleSeverity[];
-
-let enabled = [...severities];
-
-export function disableConsole(severities: TConsoleSeverity[] = enabled) {
- enabled = enabled.filter((s) => !severities.includes(s));
-}
-
-export function enableConsole() {
- enabled = [...severities];
-}
+export type TConsoleSeverity = keyof typeof __consoles;
+const level = env("cli_log_level", "log");
+const keys = Object.keys(__consoles);
export const $console = new Proxy(
{},
{
get: (_, prop) => {
- if (prop in originalConsoles && enabled.includes(prop as TConsoleSeverity)) {
+ if (prop === "original") {
+ return console;
+ }
+
+ const current = keys.indexOf(level as string);
+ const requested = keys.indexOf(prop as string);
+ if (prop in __consoles && requested <= current) {
return (...args: any[]) => __tty(prop, args);
}
return () => null;
- }
- }
-) as typeof console;
-
-export async function withDisabledConsole(
- fn: () => Promise,
- sev?: TConsoleSeverity[]
-): Promise {
- disableConsole(sev);
- try {
- const result = await fn();
- enableConsole();
- return result;
- } catch (e) {
- enableConsole();
- throw e;
- }
-}
+ },
+ },
+) as typeof console & {
+ original: typeof console;
+};
export function colorizeConsole(con: typeof console) {
- for (const [key] of Object.entries(originalConsoles)) {
+ for (const [key] of Object.entries(__consoles)) {
con[key] = $console[key];
}
}
diff --git a/app/src/core/env.ts b/app/src/core/env.ts
index 386ec9d..0dd60ec 100644
--- a/app/src/core/env.ts
+++ b/app/src/core/env.ts
@@ -1,27 +1,72 @@
-type TURSO_DB = {
- url: string;
- authToken: string;
-};
+export type Env = {};
-export type Env = {
- __STATIC_CONTENT: Fetcher;
- ENVIRONMENT: string;
- CACHE: KVNamespace;
-
- // db
- DB_DATA: TURSO_DB;
- DB_SCHEMA: TURSO_DB;
-
- // storage
- STORAGE: { access_key: string; secret_access_key: string; url: string };
- BUCKET: R2Bucket;
+export const is_toggled = (given: unknown): boolean => {
+ return typeof given === "string" ? [1, "1", "true"].includes(given) : Boolean(given);
};
export function isDebug(): boolean {
try {
// @ts-expect-error - this is a global variable in dev
- return __isDev === "1" || __isDev === 1;
+ return is_toggled(__isDev);
} catch (e) {
return false;
}
}
+
+const envs = {
+ // used in $console to determine the log level
+ cli_log_level: {
+ key: "BKND_CLI_LOG_LEVEL",
+ validate: (v: unknown) => {
+ if (
+ typeof v === "string" &&
+ ["log", "info", "warn", "error", "debug"].includes(v.toLowerCase())
+ ) {
+ return v.toLowerCase() as keyof typeof console;
+ }
+ return undefined;
+ },
+ },
+ // cli create, determine ref to download template
+ cli_create_ref: {
+ key: "BKND_CLI_CREATE_REF",
+ validate: (v: unknown) => {
+ return typeof v === "string" ? v : undefined;
+ },
+ },
+ // module manager debug: {
+ modules_debug: {
+ key: "BKND_MODULES_DEBUG",
+ validate: is_toggled,
+ },
+} as const;
+
+export const env = <
+ Key extends keyof typeof envs,
+ Fallback = any,
+ R = ReturnType<(typeof envs)[Key]["validate"]>,
+>(
+ key: Key,
+ fallback?: Fallback,
+ opts?: {
+ source?: any;
+ onFallback?: (given: unknown) => void;
+ onValid?: (valid: R) => void;
+ },
+): R extends undefined ? Fallback : R => {
+ try {
+ const source = opts?.source ?? process.env;
+ const c = envs[key];
+ const g = source[c.key];
+ const v = c.validate(g) as any;
+ if (typeof v !== "undefined") {
+ opts?.onValid?.(v);
+ return v;
+ }
+ opts?.onFallback?.(g);
+ } catch (e) {
+ opts?.onFallback?.(undefined);
+ }
+
+ return fallback as any;
+};
diff --git a/app/src/core/errors.ts b/app/src/core/errors.ts
index d9a1cdc..9173b4b 100644
--- a/app/src/core/errors.ts
+++ b/app/src/core/errors.ts
@@ -19,7 +19,7 @@ export class Exception extends Error {
return {
error: this.message,
type: this.name,
- context: this._context
+ context: this._context,
};
}
}
@@ -28,7 +28,7 @@ export class BkndError extends Error {
constructor(
message: string,
public details?: Record,
- public type?: string
+ public type?: string,
) {
super(message);
}
@@ -41,7 +41,7 @@ export class BkndError extends Error {
return {
type: this.type ?? "unknown",
message: this.message,
- details: this.details
+ details: this.details,
};
}
}
diff --git a/app/src/core/events/Event.ts b/app/src/core/events/Event.ts
index 8073c12..9defb4c 100644
--- a/app/src/core/events/Event.ts
+++ b/app/src/core/events/Event.ts
@@ -20,7 +20,7 @@ export abstract class Event {
protected clone = Event>(
this: This,
- params: Params
+ params: Params,
): This {
const cloned = new (this.constructor as any)(params);
cloned.returned = true;
@@ -50,7 +50,7 @@ export class InvalidEventReturn extends Error {
export class EventReturnedWithoutValidation extends Error {
constructor(
event: EventClass,
- public data: any
+ public data: any,
) {
// @ts-expect-error slug is static
super(`Event "${event.constructor.slug}" returned without validation`);
diff --git a/app/src/core/events/EventListener.ts b/app/src/core/events/EventListener.ts
index d3ac6ff..76a067e 100644
--- a/app/src/core/events/EventListener.ts
+++ b/app/src/core/events/EventListener.ts
@@ -6,7 +6,7 @@ export type ListenerMode = (typeof ListenerModes)[number];
export type ListenerHandler> = (
event: E,
- slug: string
+ slug: string,
) => E extends Event ? R | Promise : never;
export class EventListener {
@@ -20,7 +20,7 @@ export class EventListener {
event: EventClass,
handler: ListenerHandler,
mode: ListenerMode = "async",
- id?: string
+ id?: string,
) {
this.event = event;
this.handler = handler;
diff --git a/app/src/core/events/EventManager.ts b/app/src/core/events/EventManager.ts
index 26549ba..c2de8c9 100644
--- a/app/src/core/events/EventManager.ts
+++ b/app/src/core/events/EventManager.ts
@@ -17,7 +17,7 @@ export interface EmitsEvents {
export type { EventClass };
export class EventManager<
- RegisteredEvents extends Record = Record
+ RegisteredEvents extends Record = Record,
> {
protected events: EventClass[] = [];
protected listeners: EventListener[] = [];
@@ -30,7 +30,7 @@ export class EventManager<
onError?: (event: Event, e: unknown) => void;
onInvalidReturn?: (event: Event, e: InvalidEventReturn) => void;
asyncExecutor?: typeof Promise.all;
- }
+ },
) {
if (events) {
this.registerEvents(events);
@@ -69,7 +69,7 @@ export class EventManager<
return new Proxy(this, {
get: (_, prop: string) => {
return this.events.find((e) => e.slug === prop);
- }
+ },
}) as any;
}
@@ -141,7 +141,7 @@ export class EventManager<
protected createEventListener(
_event: EventClass | string,
handler: ListenerHandler,
- _config: RegisterListenerConfig = "async"
+ _config: RegisterListenerConfig = "async",
) {
const event =
typeof _event === "string" ? this.events.find((e) => e.slug === _event)! : _event;
@@ -159,7 +159,7 @@ export class EventManager<
onEvent>(
event: ActualEvent,
handler: ListenerHandler,
- config?: RegisterListenerConfig
+ config?: RegisterListenerConfig,
) {
this.createEventListener(event, handler, config);
}
@@ -167,7 +167,7 @@ export class EventManager<
on(
slug: string,
handler: ListenerHandler>,
- config?: RegisterListenerConfig
+ config?: RegisterListenerConfig,
) {
this.createEventListener(slug, handler, config);
}
@@ -225,7 +225,7 @@ export class EventManager<
if (!newEvent.returned) {
throw new Error(
// @ts-expect-error slug is static
- `Returned event ${newEvent.constructor.slug} must be marked as returned.`
+ `Returned event ${newEvent.constructor.slug} must be marked as returned.`,
);
}
_event = newEvent as Actual;
diff --git a/app/src/core/events/index.ts b/app/src/core/events/index.ts
index 1edb065..b60ff04 100644
--- a/app/src/core/events/index.ts
+++ b/app/src/core/events/index.ts
@@ -3,6 +3,6 @@ export {
EventListener,
ListenerModes,
type ListenerMode,
- type ListenerHandler
+ type ListenerHandler,
} from "./EventListener";
export { EventManager, type EmitsEvents, type EventClass } from "./EventManager";
diff --git a/app/src/core/index.ts b/app/src/core/index.ts
index 9f13b20..5c63a4b 100644
--- a/app/src/core/index.ts
+++ b/app/src/core/index.ts
@@ -2,14 +2,14 @@ import type { Hono, MiddlewareHandler } from "hono";
export { tbValidator } from "./server/lib/tbValidator";
export { Exception, BkndError } from "./errors";
-export { isDebug } from "./env";
+export { isDebug, env } from "./env";
export { type PrimaryFieldType, config, type DB } from "./config";
export { AwsClient } from "./clients/aws/AwsClient";
export {
SimpleRenderer,
type TemplateObject,
type TemplateTypes,
- type SimpleRendererOptions
+ type SimpleRendererOptions,
} from "./template/SimpleRenderer";
export { SchemaObject } from "./object/SchemaObject";
export { DebugLogger } from "./utils/DebugLogger";
@@ -22,7 +22,7 @@ export {
isPrimitive,
type TExpression,
type BooleanLike,
- isBooleanLike
+ isBooleanLike,
} from "./object/query/query";
export { Registry, type Constructor } from "./registry/Registry";
diff --git a/app/src/core/object/SchemaObject.ts b/app/src/core/object/SchemaObject.ts
index c8584bf..8151c8a 100644
--- a/app/src/core/object/SchemaObject.ts
+++ b/app/src/core/object/SchemaObject.ts
@@ -6,14 +6,14 @@ import {
getFullPathKeys,
mergeObjectWith,
parse,
- stripMark
+ stripMark,
} from "../utils";
export type SchemaObjectOptions = {
onUpdate?: (config: Static) => void | Promise;
onBeforeUpdate?: (
from: Static,
- to: Static
+ to: Static,
) => Static | Promise>;
restrictPaths?: string[];
overwritePaths?: (RegExp | string)[];
@@ -29,13 +29,13 @@ export class SchemaObject {
constructor(
private _schema: Schema,
initial?: Partial>,
- private options?: SchemaObjectOptions
+ private options?: SchemaObjectOptions,
) {
this._default = Default(_schema, {} as any) as any;
this._value = initial
? parse(_schema, structuredClone(initial as any), {
forceParse: this.isForceParse(),
- skipMark: this.isForceParse()
+ skipMark: this.isForceParse(),
})
: this._default;
this._config = Object.freeze(this._value);
@@ -71,7 +71,7 @@ export class SchemaObject {
async set(config: Static, noEmit?: boolean): Promise> {
const valid = parse(this._schema, structuredClone(config) as any, {
forceParse: true,
- skipMark: this.isForceParse()
+ skipMark: this.isForceParse(),
});
// regardless of "noEmit" – this should always be triggered
const updatedConfig = await this.onBeforeUpdate(this._config, valid);
@@ -159,7 +159,7 @@ export class SchemaObject {
overwritePaths.some((k2) => {
//console.log("keep?", { k, k2 }, k2 !== k && k2.startsWith(k));
return k2 !== k && k2.startsWith(k);
- })
+ }),
)
: overwritePaths;
//console.log("specific", specific);
diff --git a/app/src/core/object/diff.ts b/app/src/core/object/diff.ts
index 9a182bd..10fa427 100644
--- a/app/src/core/object/diff.ts
+++ b/app/src/core/object/diff.ts
@@ -1,7 +1,7 @@
enum Change {
Add = "a",
Remove = "r",
- Edit = "e"
+ Edit = "e",
}
type Object = object;
@@ -50,7 +50,7 @@ function diff(oldObj: Object, newObj: Object): DiffEntry[] {
t: Change.Edit,
p: path,
o: oldValue,
- n: newValue
+ n: newValue,
});
} else if (Array.isArray(oldValue) && Array.isArray(newValue)) {
const maxLength = Math.max(oldValue.length, newValue.length);
@@ -60,14 +60,14 @@ function diff(oldObj: Object, newObj: Object): DiffEntry[] {
t: Change.Add,
p: [...path, i],
o: undefined,
- n: newValue[i]
+ n: newValue[i],
});
} else if (i >= newValue.length) {
diffs.push({
t: Change.Remove,
p: [...path, i],
o: oldValue[i],
- n: undefined
+ n: undefined,
});
} else {
recurse(oldValue[i], newValue[i], [...path, i]);
@@ -83,14 +83,14 @@ function diff(oldObj: Object, newObj: Object): DiffEntry[] {
t: Change.Add,
p: [...path, key],
o: undefined,
- n: newValue[key]
+ n: newValue[key],
});
} else if (!(key in newValue)) {
diffs.push({
t: Change.Remove,
p: [...path, key],
o: oldValue[key],
- n: undefined
+ n: undefined,
});
} else {
recurse(oldValue[key], newValue[key], [...path, key]);
@@ -101,7 +101,7 @@ function diff(oldObj: Object, newObj: Object): DiffEntry[] {
t: Change.Edit,
p: path,
o: oldValue,
- n: newValue
+ n: newValue,
});
}
}
diff --git a/app/src/core/object/query/object-query.ts b/app/src/core/object/query/object-query.ts
index c4c802e..2e6a799 100644
--- a/app/src/core/object/query/object-query.ts
+++ b/app/src/core/object/query/object-query.ts
@@ -4,12 +4,12 @@ const expressions = [
exp(
"$eq",
(v: Primitive) => isPrimitive(v),
- (e, a) => e === a
+ (e, a) => e === a,
),
exp(
"$ne",
(v: Primitive) => isPrimitive(v),
- (e, a) => e !== a
+ (e, a) => e !== a,
),
exp(
"$like",
@@ -25,7 +25,7 @@ const expressions = [
default:
return false;
}
- }
+ },
),
exp(
"$regex",
@@ -39,54 +39,54 @@ const expressions = [
return regex.test(a);
}
return false;
- }
+ },
),
exp(
"$isnull",
(v: boolean | 1 | 0) => true,
- (e, a) => (e ? a === null : a !== null)
+ (e, a) => (e ? a === null : a !== null),
),
exp(
"$notnull",
(v: boolean | 1 | 0) => true,
- (e, a) => (e ? a !== null : a === null)
+ (e, a) => (e ? a !== null : a === null),
),
exp(
"$in",
(v: (string | number)[]) => Array.isArray(v),
- (e: any, a: any) => e.includes(a)
+ (e: any, a: any) => e.includes(a),
),
exp(
"$notin",
(v: (string | number)[]) => Array.isArray(v),
- (e: any, a: any) => !e.includes(a)
+ (e: any, a: any) => !e.includes(a),
),
exp(
"$gt",
(v: number) => typeof v === "number",
- (e: any, a: any) => a > e
+ (e: any, a: any) => a > e,
),
exp(
"$gte",
(v: number) => typeof v === "number",
- (e: any, a: any) => a >= e
+ (e: any, a: any) => a >= e,
),
exp(
"$lt",
(v: number) => typeof v === "number",
- (e: any, a: any) => a < e
+ (e: any, a: any) => a < e,
),
exp(
"$lte",
(v: number) => typeof v === "number",
- (e: any, a: any) => a <= e
+ (e: any, a: any) => a <= e,
),
exp(
"$between",
(v: [number, number]) =>
Array.isArray(v) && v.length === 2 && v.every((n) => typeof n === "number"),
- (e: any, a: any) => e[0] <= a && a <= e[1]
- )
+ (e: any, a: any) => e[0] <= a && a <= e[1],
+ ),
];
export type ObjectQuery = FilterQuery;
diff --git a/app/src/core/object/query/query.ts b/app/src/core/object/query/query.ts
index e30979e..c432daf 100644
--- a/app/src/core/object/query/query.ts
+++ b/app/src/core/object/query/query.ts
@@ -1,4 +1,6 @@
-export type Primitive = string | number | boolean;
+import type { PrimaryFieldType } from "core";
+
+export type Primitive = PrimaryFieldType | string | number | boolean;
export function isPrimitive(value: any): value is Primitive {
return ["string", "number", "boolean"].includes(typeof value);
}
@@ -13,7 +15,7 @@ export class Expression {
constructor(
public key: Key,
public valid: (v: Expect) => boolean,
- public validate: (e: any, a: any, ctx: CTX) => any
+ public validate: (e: any, a: any, ctx: CTX) => any,
) {}
}
export type TExpression = Expression;
@@ -21,7 +23,7 @@ export type TExpression = Expression(
key: Key,
valid: (v: Expect) => boolean,
- validate: (e: Expect, a: unknown, ctx: CTX) => any
+ validate: (e: Expect, a: unknown, ctx: CTX) => any,
): Expression {
return new Expression(key, valid, validate);
}
@@ -38,7 +40,7 @@ type ExpressionCondition = {
function getExpression(
expressions: Exps,
- key: string
+ key: string,
): Expression {
const exp = expressions.find((e) => e.key === key);
if (!exp) throw new Error(`Expression does not exist: "${key}"`);
@@ -61,9 +63,8 @@ export type FilterQuery =
function _convert(
$query: FilterQuery,
expressions: Exps,
- path: string[] = []
+ path: string[] = [],
): FilterQuery {
- //console.log("-----------------");
const ExpressionConditionKeys = expressions.map((e) => e.key);
const keys = Object.keys($query);
const operands = [OperandOr] as const;
@@ -98,7 +99,7 @@ function _convert(
} else if (typeof value === "object") {
// when object is given, check if all keys are expressions
const invalid = Object.keys(value).filter(
- (f) => !ExpressionConditionKeys.includes(f as any)
+ (f) => !ExpressionConditionKeys.includes(f as any),
);
if (invalid.length === 0) {
newQuery[key] = {};
@@ -109,7 +110,7 @@ function _convert(
}
} else {
throw new Error(
- `Invalid key(s) at "${key}": ${invalid.join(", ")}. Expected expressions.`
+ `Invalid key(s) at "${key}": ${invalid.join(", ")}. Expected expressions.`,
);
}
}
@@ -128,16 +129,14 @@ type BuildOptions = {
function _build(
_query: FilterQuery,
expressions: Exps,
- options: BuildOptions
+ options: BuildOptions,
): ValidationResults {
const $query = options.convert ? _convert(_query, expressions) : _query;
- //console.log("-----------------", { $query });
- //const keys = Object.keys($query);
const result: ValidationResults = {
$and: [],
$or: [],
- keys: new Set()
+ keys: new Set(),
};
const { $or, ...$and } = $query;
@@ -150,22 +149,16 @@ function _build(
if (!exp.valid(expected)) {
throw new Error(`Invalid expected value at "${[...path, $op].join(".")}": ${expected}`);
}
- //console.log("found exp", { key: exp.key, expected, actual });
return exp.validate(expected, actual, options.exp_ctx);
}
// check $and
- //console.log("$and entries", Object.entries($and));
for (const [key, value] of Object.entries($and)) {
- //console.log("$op/$v", Object.entries(value));
for (const [$op, $v] of Object.entries(value)) {
const objValue = options.value_is_kv ? key : options.object[key];
- //console.log("--check $and", { key, value, objValue, v_i_kv: options.value_is_kv });
- //console.log("validate", { $op, $v, objValue, key });
result.$and.push(__validate($op, $v, objValue, [key]));
result.keys.add(key);
}
- //console.log("-", { key, value });
}
// check $or
@@ -173,21 +166,18 @@ function _build(
const objValue = options.value_is_kv ? key : options.object[key];
for (const [$op, $v] of Object.entries(value)) {
- //console.log("validate", { $op, $v, objValue });
result.$or.push(__validate($op, $v, objValue, [key]));
result.keys.add(key);
}
- //console.log("-", { key, value });
}
- //console.log("matches", matches);
return result;
}
function _validate(results: ValidationResults): boolean {
const matches: { $and?: boolean; $or?: boolean } = {
$and: undefined,
- $or: undefined
+ $or: undefined,
};
matches.$and = results.$and.every((r) => Boolean(r));
@@ -204,6 +194,6 @@ export function makeValidator(expressions: Exps) {
validate: (query: FilterQuery, options: BuildOptions) => {
const fns = _build(query, expressions, options);
return _validate(fns);
- }
+ },
};
}
diff --git a/app/src/core/registry/Registry.ts b/app/src/core/registry/Registry.ts
index b895f51..7c56070 100644
--- a/app/src/core/registry/Registry.ts
+++ b/app/src/core/registry/Registry.ts
@@ -5,7 +5,7 @@ export type RegisterFn- = (unknown: any) => Item;
export class Registry<
Item,
Items extends Record = Record,
- Fn extends RegisterFn
- = RegisterFn
-
+ Fn extends RegisterFn
- = RegisterFn
- ,
> {
private is_set: boolean = false;
private items: Items = {} as Items;
diff --git a/app/src/core/security/Permission.ts b/app/src/core/security/Permission.ts
index eb72ccb..86cf46b 100644
--- a/app/src/core/security/Permission.ts
+++ b/app/src/core/security/Permission.ts
@@ -5,7 +5,7 @@ export class Permission {
toJSON() {
return {
- name: this.name
+ name: this.name,
};
}
}
diff --git a/app/src/core/server/flash.ts b/app/src/core/server/flash.ts
index aeac431..6c7b259 100644
--- a/app/src/core/server/flash.ts
+++ b/app/src/core/server/flash.ts
@@ -7,7 +7,7 @@ export type FlashMessageType = "error" | "warning" | "success" | "info";
export function addFlashMessage(c: Context, message: string, type: FlashMessageType = "info") {
if (c.req.header("Accept")?.includes("text/html")) {
setCookie(c, flash_key, JSON.stringify({ type, message }), {
- path: "/"
+ path: "/",
});
}
}
@@ -28,7 +28,7 @@ function getCookieValue(name) {
}
export function getFlashMessage(
- clear = true
+ clear = true,
): { type: FlashMessageType; message: string } | undefined {
const flash = getCookieValue(flash_key);
if (flash && clear) {
diff --git a/app/src/core/server/lib/tbValidator.ts b/app/src/core/server/lib/tbValidator.ts
index 2118b18..6ae4c41 100644
--- a/app/src/core/server/lib/tbValidator.ts
+++ b/app/src/core/server/lib/tbValidator.ts
@@ -5,7 +5,7 @@ import { validator } from "hono/validator";
type Hook = (
result: { success: true; data: T } | { success: false; errors: ValueError[] },
- c: Context
+ c: Context,
) => Response | Promise | void;
export function tbValidator<
@@ -13,7 +13,7 @@ export function tbValidator<
Target extends keyof ValidationTargets,
E extends Env,
P extends string,
- V extends { in: { [K in Target]: StaticDecode }; out: { [K in Target]: StaticDecode } }
+ V extends { in: { [K in Target]: StaticDecode }; out: { [K in Target]: StaticDecode } },
>(target: Target, schema: T, hook?: Hook, E, P>): MiddlewareHandler {
// Compile the provided schema once rather than per validation. This could be optimized further using a shared schema
// compilation pool similar to the Fastify implementation.
diff --git a/app/src/core/template/SimpleRenderer.ts b/app/src/core/template/SimpleRenderer.ts
index 0e90c8e..1a16d1d 100644
--- a/app/src/core/template/SimpleRenderer.ts
+++ b/app/src/core/template/SimpleRenderer.ts
@@ -14,7 +14,7 @@ export class SimpleRenderer {
constructor(
private variables: Record = {},
- private options: SimpleRendererOptions = {}
+ private options: SimpleRendererOptions = {},
) {}
another() {
@@ -48,7 +48,7 @@ export class SimpleRenderer {
return (await this.renderString(template)) as unknown as Given;
} else if (Array.isArray(template)) {
return (await Promise.all(
- template.map((item) => this.render(item))
+ template.map((item) => this.render(item)),
)) as unknown as Given;
} else if (typeof template === "object") {
return (await this.renderObject(template)) as unknown as Given;
@@ -61,8 +61,8 @@ export class SimpleRenderer {
kind: e.token.kind,
input: e.token.input,
begin: e.token.begin,
- end: e.token.end
- }
+ end: e.token.end,
+ },
};
throw new BkndError(e.message, details, "liquid");
diff --git a/app/src/core/types.ts b/app/src/core/types.ts
index a3bf623..cfb32e4 100644
--- a/app/src/core/types.ts
+++ b/app/src/core/types.ts
@@ -1,4 +1,4 @@
export interface Serializable {
toJSON(): Json;
fromJSON(json: Json): Class;
-}
\ No newline at end of file
+}
diff --git a/app/src/core/utils/crypto.ts b/app/src/core/utils/crypto.ts
index 21a188a..00f92b9 100644
--- a/app/src/core/utils/crypto.ts
+++ b/app/src/core/utils/crypto.ts
@@ -20,7 +20,7 @@ export const hash = {
sha256: async (input: string, salt?: string, pepper?: string) =>
digest("SHA-256", input, salt, pepper),
sha1: async (input: string, salt?: string, pepper?: string) =>
- digest("SHA-1", input, salt, pepper)
+ digest("SHA-1", input, salt, pepper),
};
export async function checksum(s: any) {
diff --git a/app/src/core/utils/dates.ts b/app/src/core/utils/dates.ts
index c33b496..4628004 100644
--- a/app/src/core/utils/dates.ts
+++ b/app/src/core/utils/dates.ts
@@ -11,4 +11,21 @@ declare module "dayjs" {
dayjs.extend(weekOfYear);
+export function datetimeStringLocal(dateOrString?: string | Date | undefined): string {
+ return dayjs(dateOrString).format("YYYY-MM-DD HH:mm:ss");
+}
+
+export function datetimeStringUTC(dateOrString?: string | Date | undefined): string {
+ const date = dateOrString ? new Date(dateOrString) : new Date();
+ return date.toISOString().replace("T", " ").split(".")[0]!;
+}
+
+export function getTimezoneOffset(): number {
+ return new Date().getTimezoneOffset();
+}
+
+export function getTimezone(): string {
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
+}
+
export { dayjs };
diff --git a/app/src/core/utils/objects.ts b/app/src/core/utils/objects.ts
index 2c7c68d..e3a0135 100644
--- a/app/src/core/utils/objects.ts
+++ b/app/src/core/utils/objects.ts
@@ -14,7 +14,7 @@ export function isObject(value: unknown): value is Record {
export function omitKeys(
obj: T,
- keys_: readonly K[]
+ keys_: readonly K[],
): Omit