mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 08:06:00 +00:00
Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ab46611839 | |||
| 24226382e8 | |||
| d04c4a6191 | |||
| 06125f1afe | |||
| 8ef11aa382 | |||
| 76da14294c | |||
| c1e92e503b | |||
| 70e42a02d7 | |||
| a17fd2df67 | |||
| 0648f71a9e | |||
| 131b7dd52e | |||
| 3a79ce2cf8 | |||
| deddf00c38 | |||
| a7e3ce878a | |||
| d7ee13011f | |||
| 1d1ebff64d | |||
| ac2ae2657e | |||
| 8e987d58f2 | |||
| 386c0d3ff0 | |||
| 602235b372 | |||
| c4138ef823 | |||
| 1631bbb754 | |||
| 8c91dff94d | |||
| 50c5adce0c | |||
| 9d9aa7b7a5 | |||
| 43ec075a32 | |||
| 29ae6c6f9d | |||
| bd86f8ef91 | |||
| 25027429df | |||
| 35959aaf6d | |||
| 56fb779f99 | |||
| 134fbd6d34 | |||
| d2c75b1605 | |||
| a474e3fe3a | |||
| d81b3acb94 | |||
| 2ca66f4fc9 | |||
| a8bbb6e760 | |||
| 32459a1562 | |||
| 2395d7fe97 | |||
| b7ec4982dc | |||
| 10d606f8e9 | |||
| 5cca911e9d | |||
| 13a88be8d1 | |||
| a5264e22ee | |||
| 290498de6e | |||
| 847b08b505 | |||
| 86a7bee3d9 | |||
| 3932396084 | |||
| 94cc4042d3 | |||
| 154703f873 | |||
| 71dbfc5469 | |||
| e5924b33e5 | |||
| c25abd4a48 | |||
| 7c58f20cca | |||
| ef18d8d955 | |||
| c7c6aa7df8 | |||
| 060af0a1e3 | |||
| 44a427de67 | |||
| aa75355a69 | |||
| 110c7681b7 | |||
| ba517feab5 | |||
| 7e990feb99 | |||
| 77a6b6e7f5 | |||
| a3348122e6 | |||
| 3757157a06 | |||
| 6c2f7b32e5 | |||
| 067213ff60 | |||
| a4079804c2 | |||
| 2bd4b720bb | |||
| f3619bee26 | |||
| c54455870c | |||
| 20477a655e | |||
| 1497470d33 | |||
| abe38fe69d | |||
| 665c41f051 | |||
| 0184f47a41 | |||
| 5374afc9c8 | |||
| d6978f9873 | |||
| 3c5bd95988 | |||
| b3e02394ac | |||
| 91470d530f | |||
| 0c8f3cd22a | |||
| feeb13c053 | |||
| b55fdd7516 | |||
| 894a90fca9 | |||
| 582dbd4272 | |||
| 6eb0d2242f | |||
| 85c9f35860 | |||
| 509d6826f0 | |||
| 4693be5615 | |||
| bdc6eb55bf | |||
| 54b38401d8 | |||
| 49c2a7b4db | |||
| 6718419d41 |
+5
-1
@@ -14,13 +14,17 @@ packages/media/.env
|
|||||||
**/*/.env
|
**/*/.env
|
||||||
**/*/.dev.vars
|
**/*/.dev.vars
|
||||||
**/*/.wrangler
|
**/*/.wrangler
|
||||||
|
**/*/*.tgz
|
||||||
**/*/vite.config.ts.timestamp*
|
**/*/vite.config.ts.timestamp*
|
||||||
.history
|
.history
|
||||||
**/*/.db/*
|
**/*/.db/*
|
||||||
|
**/*/*.db
|
||||||
**/*/*.db-shm
|
**/*/*.db-shm
|
||||||
**/*/*.db-wal
|
**/*/*.db-wal
|
||||||
|
**/*/.tmp
|
||||||
.npmrc
|
.npmrc
|
||||||
/.verdaccio
|
/.verdaccio
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
.git_old
|
.git_old
|
||||||
|
docker/tmp
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { mark, stripMark } from "../src/core/utils";
|
import { mark, stripMark } from "../src/core/utils";
|
||||||
import { ModuleManager } from "../src/modules/ModuleManager";
|
import { entity, text } from "../src/data";
|
||||||
import { CURRENT_VERSION, TABLE_NAME, migrateSchema } from "../src/modules/migrations";
|
import { ModuleManager, getDefaultConfig } from "../src/modules/ModuleManager";
|
||||||
|
import { CURRENT_VERSION, TABLE_NAME } from "../src/modules/migrations";
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
|
|
||||||
describe("ModuleManager", async () => {
|
describe("ModuleManager", async () => {
|
||||||
@@ -29,21 +30,68 @@ describe("ModuleManager", async () => {
|
|||||||
const mm = new ModuleManager(c.dummyConnection);
|
const mm = new ModuleManager(c.dummyConnection);
|
||||||
await mm.build();
|
await mm.build();
|
||||||
const version = mm.version();
|
const version = mm.version();
|
||||||
const json = mm.configs();
|
const configs = mm.configs();
|
||||||
|
const json = stripMark({
|
||||||
|
...configs,
|
||||||
|
data: {
|
||||||
|
...configs.data,
|
||||||
|
basepath: "/api/data2",
|
||||||
|
entities: {
|
||||||
|
test: entity("test", {
|
||||||
|
content: text()
|
||||||
|
}).toJSON()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
//const { version, ...json } = mm.toJSON() as any;
|
//const { version, ...json } = mm.toJSON() as any;
|
||||||
|
|
||||||
const c2 = getDummyConnection();
|
const c2 = getDummyConnection();
|
||||||
const db = c2.dummyConnection.kysely;
|
const db = c2.dummyConnection.kysely;
|
||||||
await migrateSchema(CURRENT_VERSION, { db });
|
const mm2 = new ModuleManager(c2.dummyConnection, { initial: { version, ...json } });
|
||||||
|
await mm2.syncConfigTable();
|
||||||
await db
|
await db
|
||||||
.updateTable(TABLE_NAME)
|
.insertInto(TABLE_NAME)
|
||||||
.set({ json: JSON.stringify(json), version: CURRENT_VERSION })
|
.values({ type: "config", json: JSON.stringify(json), version: CURRENT_VERSION })
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
const mm2 = new ModuleManager(c2.dummyConnection, { initial: { version, ...json } });
|
|
||||||
await mm2.build();
|
await mm2.build();
|
||||||
|
|
||||||
expect(json).toEqual(mm2.configs());
|
expect(json).toEqual(stripMark(mm2.configs()));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("s3.1: (fetch) config given, table exists, version matches", async () => {
|
||||||
|
const configs = getDefaultConfig();
|
||||||
|
const json = {
|
||||||
|
...configs,
|
||||||
|
data: {
|
||||||
|
...configs.data,
|
||||||
|
basepath: "/api/data2",
|
||||||
|
entities: {
|
||||||
|
test: entity("test", {
|
||||||
|
content: text()
|
||||||
|
}).toJSON()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
//const { version, ...json } = mm.toJSON() as any;
|
||||||
|
|
||||||
|
const { dummyConnection } = getDummyConnection();
|
||||||
|
const db = dummyConnection.kysely;
|
||||||
|
const mm2 = new ModuleManager(dummyConnection);
|
||||||
|
await mm2.syncConfigTable();
|
||||||
|
// assume an initial version
|
||||||
|
await db.insertInto(TABLE_NAME).values({ type: "config", json: null, version: 1 }).execute();
|
||||||
|
await db
|
||||||
|
.insertInto(TABLE_NAME)
|
||||||
|
.values({ type: "config", json: JSON.stringify(json), version: CURRENT_VERSION })
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await mm2.build();
|
||||||
|
|
||||||
|
expect(stripMark(json)).toEqual(stripMark(mm2.configs()));
|
||||||
|
expect(mm2.configs().data.entities.test).toBeDefined();
|
||||||
|
expect(mm2.configs().data.entities.test.fields.content).toBeDefined();
|
||||||
|
expect(mm2.get("data").toJSON().entities.test.fields.content).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("s4: config given, table exists, version outdated, migrate", async () => {
|
test("s4: config given, table exists, version outdated, migrate", async () => {
|
||||||
@@ -52,21 +100,19 @@ describe("ModuleManager", async () => {
|
|||||||
await mm.build();
|
await mm.build();
|
||||||
const version = mm.version();
|
const version = mm.version();
|
||||||
const json = mm.configs();
|
const json = mm.configs();
|
||||||
//const { version, ...json } = mm.toJSON() as any;
|
|
||||||
|
|
||||||
const c2 = getDummyConnection();
|
const c2 = getDummyConnection();
|
||||||
const db = c2.dummyConnection.kysely;
|
const db = c2.dummyConnection.kysely;
|
||||||
console.log("here2");
|
|
||||||
await migrateSchema(CURRENT_VERSION, { db });
|
|
||||||
await db
|
|
||||||
.updateTable(TABLE_NAME)
|
|
||||||
.set({ json: JSON.stringify(json), version: CURRENT_VERSION - 1 })
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
const mm2 = new ModuleManager(c2.dummyConnection, {
|
const mm2 = new ModuleManager(c2.dummyConnection, {
|
||||||
initial: { version: version - 1, ...json }
|
initial: { version: version - 1, ...json }
|
||||||
});
|
});
|
||||||
console.log("here3");
|
await mm2.syncConfigTable();
|
||||||
|
|
||||||
|
await db
|
||||||
|
.insertInto(TABLE_NAME)
|
||||||
|
.values({ json: JSON.stringify(json), type: "config", version: CURRENT_VERSION - 1 })
|
||||||
|
.execute();
|
||||||
|
|
||||||
await mm2.build();
|
await mm2.build();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -80,15 +126,15 @@ describe("ModuleManager", async () => {
|
|||||||
|
|
||||||
const c2 = getDummyConnection();
|
const c2 = getDummyConnection();
|
||||||
const db = c2.dummyConnection.kysely;
|
const db = c2.dummyConnection.kysely;
|
||||||
await migrateSchema(CURRENT_VERSION, { db });
|
|
||||||
await db
|
|
||||||
.updateTable(TABLE_NAME)
|
|
||||||
.set({ json: JSON.stringify(json), version: CURRENT_VERSION })
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
const mm2 = new ModuleManager(c2.dummyConnection, {
|
const mm2 = new ModuleManager(c2.dummyConnection, {
|
||||||
initial: { version: version - 1, ...json }
|
initial: { version: version - 1, ...json }
|
||||||
});
|
});
|
||||||
|
await mm2.syncConfigTable();
|
||||||
|
await db
|
||||||
|
.insertInto(TABLE_NAME)
|
||||||
|
.values({ type: "config", json: JSON.stringify(json), version: CURRENT_VERSION })
|
||||||
|
.execute();
|
||||||
|
|
||||||
expect(mm2.build()).rejects.toThrow(/version.*do not match/);
|
expect(mm2.build()).rejects.toThrow(/version.*do not match/);
|
||||||
});
|
});
|
||||||
@@ -102,7 +148,9 @@ describe("ModuleManager", async () => {
|
|||||||
|
|
||||||
const c2 = getDummyConnection();
|
const c2 = getDummyConnection();
|
||||||
const db = c2.dummyConnection.kysely;
|
const db = c2.dummyConnection.kysely;
|
||||||
await migrateSchema(CURRENT_VERSION, { db });
|
|
||||||
|
const mm2 = new ModuleManager(c2.dummyConnection);
|
||||||
|
await mm2.syncConfigTable();
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
...json,
|
...json,
|
||||||
@@ -112,12 +160,11 @@ describe("ModuleManager", async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
await db
|
await db
|
||||||
.updateTable(TABLE_NAME)
|
.insertInto(TABLE_NAME)
|
||||||
.set({ json: JSON.stringify(config), version: CURRENT_VERSION })
|
.values({ type: "config", json: JSON.stringify(config), version: CURRENT_VERSION })
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
// run without config given
|
// run without config given
|
||||||
const mm2 = new ModuleManager(c2.dummyConnection);
|
|
||||||
await mm2.build();
|
await mm2.build();
|
||||||
|
|
||||||
expect(mm2.configs().data.basepath).toBe("/api/data2");
|
expect(mm2.configs().data.basepath).toBe("/api/data2");
|
||||||
@@ -148,50 +195,61 @@ describe("ModuleManager", async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// @todo: check what happens here
|
test("partial config given", async () => {
|
||||||
/*test("blank app, modify deep config", async () => {
|
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
|
|
||||||
const mm = new ModuleManager(dummyConnection);
|
const partial = {
|
||||||
|
auth: {
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const mm = new ModuleManager(dummyConnection, {
|
||||||
|
initial: partial
|
||||||
|
});
|
||||||
await mm.build();
|
await mm.build();
|
||||||
|
|
||||||
/!* await mm
|
expect(mm.version()).toBe(CURRENT_VERSION);
|
||||||
.get("data")
|
expect(mm.built()).toBe(true);
|
||||||
.schema()
|
expect(mm.configs().auth.enabled).toBe(true);
|
||||||
.patch("entities.test", {
|
expect(mm.configs().data.entities.users).toBeDefined();
|
||||||
fields: {
|
});
|
||||||
content: {
|
|
||||||
type: "text"
|
test("partial config given, but db version exists", async () => {
|
||||||
}
|
const c = getDummyConnection();
|
||||||
|
const mm = new ModuleManager(c.dummyConnection);
|
||||||
|
await mm.build();
|
||||||
|
const json = mm.configs();
|
||||||
|
|
||||||
|
const c2 = getDummyConnection();
|
||||||
|
const db = c2.dummyConnection.kysely;
|
||||||
|
|
||||||
|
const mm2 = new ModuleManager(c2.dummyConnection, {
|
||||||
|
initial: {
|
||||||
|
auth: {
|
||||||
|
basepath: "/shouldnt/take/this"
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
await mm.build();
|
});
|
||||||
|
await mm2.syncConfigTable();
|
||||||
|
const payload = {
|
||||||
|
...json,
|
||||||
|
auth: {
|
||||||
|
...json.auth,
|
||||||
|
enabled: true,
|
||||||
|
basepath: "/api/auth2"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await db
|
||||||
|
.insertInto(TABLE_NAME)
|
||||||
|
.values({
|
||||||
|
type: "config",
|
||||||
|
json: JSON.stringify(payload),
|
||||||
|
version: CURRENT_VERSION
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
await mm2.build();
|
||||||
|
expect(mm2.configs().auth.basepath).toBe("/api/auth2");
|
||||||
|
});
|
||||||
|
|
||||||
expect(mm.configs().data.entities?.users?.fields?.email.type).toBe("text");
|
// @todo: add tests for migrations (check "backup" and new version)
|
||||||
|
|
||||||
expect(
|
|
||||||
mm.get("data").schema().patch("desc", "entities.users.config.sort_dir")
|
|
||||||
).rejects.toThrow();
|
|
||||||
await mm.build();*!/
|
|
||||||
expect(mm.configs().data.entities?.users?.fields?.email.type).toBe("text");
|
|
||||||
console.log("here", mm.configs());
|
|
||||||
await mm
|
|
||||||
.get("data")
|
|
||||||
.schema()
|
|
||||||
.patch("entities.users", { config: { sort_dir: "desc" } });
|
|
||||||
await mm.build();
|
|
||||||
expect(mm.toJSON());
|
|
||||||
|
|
||||||
//console.log(_jsonp(mm.toJSON().data));
|
|
||||||
/!*expect(mm.configs().data.entities!.test!.fields!.content.type).toBe("text");
|
|
||||||
expect(mm.configs().data.entities!.users!.config!.sort_dir).toBe("desc");*!/
|
|
||||||
});*/
|
|
||||||
|
|
||||||
/*test("accessing modules", async () => {
|
|
||||||
const { dummyConnection } = getDummyConnection();
|
|
||||||
|
|
||||||
const mm = new ModuleManager(dummyConnection);
|
|
||||||
|
|
||||||
//mm.get("auth").mutate().set({});
|
|
||||||
});*/
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
import { Hono } from "hono";
|
||||||
|
import { secureRandomString } from "../../src/core/utils";
|
||||||
|
import { ModuleApi } from "../../src/modules";
|
||||||
|
|
||||||
|
class Api extends ModuleApi {
|
||||||
|
_getUrl(path: string) {
|
||||||
|
return this.getUrl(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const host = "http://localhost";
|
||||||
|
|
||||||
|
describe("ModuleApi", () => {
|
||||||
|
it("resolves options correctly", () => {
|
||||||
|
const api = new Api({ host });
|
||||||
|
expect(api.options).toEqual({ host });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns correct url from path", () => {
|
||||||
|
const api = new Api({ host });
|
||||||
|
expect(api._getUrl("/test")).toEqual("http://localhost/test");
|
||||||
|
expect(api._getUrl("test")).toEqual("http://localhost/test");
|
||||||
|
expect(api._getUrl("test/")).toEqual("http://localhost/test");
|
||||||
|
expect(api._getUrl("//test?foo=1")).toEqual("http://localhost/test?foo=1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fetches endpoint", async () => {
|
||||||
|
const app = new Hono().get("/endpoint", (c) => c.json({ foo: "bar" }));
|
||||||
|
const api = new Api({ host });
|
||||||
|
api.fetcher = app.request as typeof fetch;
|
||||||
|
|
||||||
|
const res = await api.get("/endpoint");
|
||||||
|
expect(res.res.ok).toEqual(true);
|
||||||
|
expect(res.res.status).toEqual(200);
|
||||||
|
expect(res.data).toEqual({ foo: "bar" });
|
||||||
|
expect(res.body).toEqual({ foo: "bar" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has accessible request", async () => {
|
||||||
|
const app = new Hono().get("/endpoint", (c) => c.json({ foo: "bar" }));
|
||||||
|
const api = new Api({ host });
|
||||||
|
api.fetcher = app.request as typeof fetch;
|
||||||
|
|
||||||
|
const promise = api.get("/endpoint");
|
||||||
|
expect(promise.request).toBeDefined();
|
||||||
|
expect(promise.request.url).toEqual("http://localhost/endpoint");
|
||||||
|
|
||||||
|
expect((await promise).body).toEqual({ foo: "bar" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds token to headers when given in options", () => {
|
||||||
|
const token = secureRandomString(20);
|
||||||
|
const api = new Api({ host, token, token_transport: "header" });
|
||||||
|
|
||||||
|
expect(api.get("/").request.headers.get("Authorization")).toEqual(`Bearer ${token}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets header to accept json", () => {
|
||||||
|
const api = new Api({ host });
|
||||||
|
expect(api.get("/").request.headers.get("Accept")).toEqual("application/json");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds additional headers from options", () => {
|
||||||
|
const headers = new Headers({
|
||||||
|
"X-Test": "123"
|
||||||
|
});
|
||||||
|
const api = new Api({ host, headers });
|
||||||
|
expect(api.get("/").request.headers.get("X-Test")).toEqual("123");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses basepath & removes trailing slash", () => {
|
||||||
|
const api = new Api({ host, basepath: "/api" });
|
||||||
|
expect(api.get("/").request.url).toEqual("http://localhost/api");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses search params", () => {
|
||||||
|
const api = new Api({ host });
|
||||||
|
const search = new URLSearchParams({
|
||||||
|
foo: "bar"
|
||||||
|
});
|
||||||
|
expect(api.get("/", search).request.url).toEqual("http://localhost/?" + search.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves method shortcut fns correctly", () => {
|
||||||
|
const api = new Api({ host });
|
||||||
|
expect(api.get("/").request.method).toEqual("GET");
|
||||||
|
expect(api.post("/").request.method).toEqual("POST");
|
||||||
|
expect(api.put("/").request.method).toEqual("PUT");
|
||||||
|
expect(api.patch("/").request.method).toEqual("PATCH");
|
||||||
|
expect(api.delete("/").request.method).toEqual("DELETE");
|
||||||
|
});
|
||||||
|
|
||||||
|
// @todo: test error response
|
||||||
|
// @todo: test method shortcut functions
|
||||||
|
});
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { describe, test } from "bun:test";
|
|
||||||
import { DataApi } from "../../src/modules/data/api/DataApi";
|
|
||||||
|
|
||||||
describe("Api", async () => {
|
|
||||||
test("...", async () => {
|
|
||||||
/*const dataApi = new DataApi({
|
|
||||||
host: "https://dev-config-soma.bknd.run"
|
|
||||||
});
|
|
||||||
|
|
||||||
const one = await dataApi.readOne("users", 1);
|
|
||||||
const many = await dataApi.readMany("users", { limit: 2 });
|
|
||||||
console.log("one", one);
|
|
||||||
console.log("many", many);*/
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import { describe, expect, it, test } from "bun:test";
|
|
||||||
import { Endpoint } from "../../src/core";
|
|
||||||
import { mockFetch2, unmockFetch } from "./helper";
|
|
||||||
|
|
||||||
const testC: any = {
|
|
||||||
json: (res: any) => Response.json(res)
|
|
||||||
};
|
|
||||||
const testNext = async () => {};
|
|
||||||
|
|
||||||
describe("Endpoint", async () => {
|
|
||||||
it("behaves as expected", async () => {
|
|
||||||
const endpoint = new Endpoint("GET", "/test", async () => {
|
|
||||||
return { hello: "test" };
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(endpoint.method).toBe("GET");
|
|
||||||
expect(endpoint.path).toBe("/test");
|
|
||||||
|
|
||||||
const handler = endpoint.toHandler();
|
|
||||||
const response = await handler(testC, testNext);
|
|
||||||
|
|
||||||
expect(response.ok).toBe(true);
|
|
||||||
expect(await response.json()).toEqual({ hello: "test" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can be $request(ed)", async () => {
|
|
||||||
const obj = { hello: "test" };
|
|
||||||
const baseUrl = "https://local.com:123";
|
|
||||||
const endpoint = Endpoint.get("/test", async () => obj);
|
|
||||||
|
|
||||||
mockFetch2(async (input: RequestInfo, init: RequestInit) => {
|
|
||||||
expect(input).toBe(`${baseUrl}/test`);
|
|
||||||
return new Response(JSON.stringify(obj), { status: 200 });
|
|
||||||
});
|
|
||||||
const response = await endpoint.$request({}, baseUrl);
|
|
||||||
|
|
||||||
expect(response).toEqual({
|
|
||||||
status: 200,
|
|
||||||
ok: true,
|
|
||||||
response: obj
|
|
||||||
});
|
|
||||||
unmockFetch();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("resolves helper functions", async () => {
|
|
||||||
const params = ["/test", () => ({ hello: "test" })];
|
|
||||||
|
|
||||||
["get", "post", "patch", "put", "delete"].forEach((method) => {
|
|
||||||
const endpoint = Endpoint[method](...params);
|
|
||||||
expect(endpoint.method).toBe(method.toUpperCase());
|
|
||||||
expect(endpoint.path).toBe(params[0]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import type { TObject, TString } from "@sinclair/typebox";
|
import type { TObject, TString } from "@sinclair/typebox";
|
||||||
import { Registry } from "../../src/core/registry/Registry";
|
import { Registry } from "../../src/core/registry/Registry";
|
||||||
import { type TSchema, Type } from "../../src/core/utils";
|
import { type TSchema, Type } from "../../src/core/utils";
|
||||||
@@ -11,6 +11,9 @@ class What {
|
|||||||
method() {
|
method() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
getType() {
|
||||||
|
return Type.Object({ type: Type.String() });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
class What2 extends What {}
|
class What2 extends What {}
|
||||||
class NotAllowed {}
|
class NotAllowed {}
|
||||||
@@ -32,25 +35,53 @@ describe("Registry", () => {
|
|||||||
} satisfies Record<string, Test1>);
|
} satisfies Record<string, Test1>);
|
||||||
|
|
||||||
const item = registry.get("first");
|
const item = registry.get("first");
|
||||||
|
expect(item).toBeDefined();
|
||||||
|
expect(item?.cls).toBe(What);
|
||||||
|
|
||||||
|
const second = Type.Object({ type: Type.String(), what: Type.String() });
|
||||||
registry.add("second", {
|
registry.add("second", {
|
||||||
cls: What2,
|
cls: What2,
|
||||||
schema: Type.Object({ type: Type.String(), what: Type.String() }),
|
schema: second,
|
||||||
enabled: true
|
enabled: true
|
||||||
});
|
});
|
||||||
|
// @ts-ignore
|
||||||
|
expect(registry.get("second").schema).toEqual(second);
|
||||||
|
|
||||||
|
const third = Type.Object({ type: Type.String({ default: "1" }), what22: Type.String() });
|
||||||
registry.add("third", {
|
registry.add("third", {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
cls: NotAllowed,
|
cls: NotAllowed,
|
||||||
schema: Type.Object({ type: Type.String({ default: "1" }), what22: Type.String() }),
|
schema: third,
|
||||||
enabled: true
|
enabled: true
|
||||||
});
|
});
|
||||||
|
// @ts-ignore
|
||||||
|
expect(registry.get("third").schema).toEqual(third);
|
||||||
|
|
||||||
|
const fourth = Type.Object({ type: Type.Number(), what22: Type.String() });
|
||||||
registry.add("fourth", {
|
registry.add("fourth", {
|
||||||
cls: What,
|
cls: What,
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
schema: Type.Object({ type: Type.Number(), what22: Type.String() }),
|
schema: fourth,
|
||||||
enabled: true
|
enabled: true
|
||||||
});
|
});
|
||||||
|
// @ts-ignore
|
||||||
|
expect(registry.get("fourth").schema).toEqual(fourth);
|
||||||
|
|
||||||
console.log("list", registry.all());
|
expect(Object.keys(registry.all()).length).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses registration fn", async () => {
|
||||||
|
const registry = new Registry<Test1>((a: ClassRef<What>) => {
|
||||||
|
return {
|
||||||
|
cls: a,
|
||||||
|
schema: a.prototype.getType(),
|
||||||
|
enabled: true
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.register("what2", What2);
|
||||||
|
expect(registry.get("what2")).toBeDefined();
|
||||||
|
expect(registry.get("what2").cls).toBe(What2);
|
||||||
|
expect(registry.get("what2").schema).toEqual(What2.prototype.getType());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,443 @@
|
|||||||
|
import { describe, expect, it, test } from "bun:test";
|
||||||
|
import { apply, diff, revert } from "../../../src/core/object/diff";
|
||||||
|
|
||||||
|
describe("diff", () => {
|
||||||
|
it("should detect added properties", () => {
|
||||||
|
const oldObj = { a: 1 };
|
||||||
|
const newObj = { a: 1, b: 2 };
|
||||||
|
|
||||||
|
const diffs = diff(oldObj, newObj);
|
||||||
|
|
||||||
|
expect(diffs).toEqual([
|
||||||
|
{
|
||||||
|
t: "a",
|
||||||
|
p: ["b"],
|
||||||
|
o: undefined,
|
||||||
|
n: 2
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const appliedObj = apply(oldObj, diffs);
|
||||||
|
expect(appliedObj).toEqual(newObj);
|
||||||
|
|
||||||
|
const revertedObj = revert(newObj, diffs);
|
||||||
|
expect(revertedObj).toEqual(oldObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should detect removed properties", () => {
|
||||||
|
const oldObj = { a: 1, b: 2 };
|
||||||
|
const newObj = { a: 1 };
|
||||||
|
|
||||||
|
const diffs = diff(oldObj, newObj);
|
||||||
|
|
||||||
|
expect(diffs).toEqual([
|
||||||
|
{
|
||||||
|
t: "r",
|
||||||
|
p: ["b"],
|
||||||
|
o: 2,
|
||||||
|
n: undefined
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const appliedObj = apply(oldObj, diffs);
|
||||||
|
expect(appliedObj).toEqual(newObj);
|
||||||
|
|
||||||
|
const revertedObj = revert(newObj, diffs);
|
||||||
|
expect(revertedObj).toEqual(oldObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should detect edited properties", () => {
|
||||||
|
const oldObj = { a: 1 };
|
||||||
|
const newObj = { a: 2 };
|
||||||
|
|
||||||
|
const diffs = diff(oldObj, newObj);
|
||||||
|
|
||||||
|
expect(diffs).toEqual([
|
||||||
|
{
|
||||||
|
t: "e",
|
||||||
|
p: ["a"],
|
||||||
|
o: 1,
|
||||||
|
n: 2
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const appliedObj = apply(oldObj, diffs);
|
||||||
|
expect(appliedObj).toEqual(newObj);
|
||||||
|
|
||||||
|
const revertedObj = revert(newObj, diffs);
|
||||||
|
expect(revertedObj).toEqual(oldObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should detect changes in nested objects", () => {
|
||||||
|
const oldObj = { a: { b: 1 } };
|
||||||
|
const newObj = { a: { b: 2 } };
|
||||||
|
|
||||||
|
const diffs = diff(oldObj, newObj);
|
||||||
|
|
||||||
|
expect(diffs).toEqual([
|
||||||
|
{
|
||||||
|
t: "e",
|
||||||
|
p: ["a", "b"],
|
||||||
|
o: 1,
|
||||||
|
n: 2
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const appliedObj = apply(oldObj, diffs);
|
||||||
|
expect(appliedObj).toEqual(newObj);
|
||||||
|
|
||||||
|
const revertedObj = revert(newObj, diffs);
|
||||||
|
expect(revertedObj).toEqual(oldObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should detect changes in arrays", () => {
|
||||||
|
const oldObj = { a: [1, 2, 3] };
|
||||||
|
const newObj = { a: [1, 4, 3, 5] };
|
||||||
|
|
||||||
|
const diffs = diff(oldObj, newObj);
|
||||||
|
|
||||||
|
expect(diffs).toEqual([
|
||||||
|
{
|
||||||
|
t: "e",
|
||||||
|
p: ["a", 1],
|
||||||
|
o: 2,
|
||||||
|
n: 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
t: "a",
|
||||||
|
p: ["a", 3],
|
||||||
|
o: undefined,
|
||||||
|
n: 5
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const appliedObj = apply(oldObj, diffs);
|
||||||
|
expect(appliedObj).toEqual(newObj);
|
||||||
|
|
||||||
|
const revertedObj = revert(newObj, diffs);
|
||||||
|
expect(revertedObj).toEqual(oldObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle adding elements to an empty array", () => {
|
||||||
|
const oldObj = { a: [] };
|
||||||
|
const newObj = { a: [1, 2, 3] };
|
||||||
|
|
||||||
|
const diffs = diff(oldObj, newObj);
|
||||||
|
|
||||||
|
expect(diffs).toEqual([
|
||||||
|
{
|
||||||
|
t: "a",
|
||||||
|
p: ["a", 0],
|
||||||
|
o: undefined,
|
||||||
|
n: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
t: "a",
|
||||||
|
p: ["a", 1],
|
||||||
|
o: undefined,
|
||||||
|
n: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
t: "a",
|
||||||
|
p: ["a", 2],
|
||||||
|
o: undefined,
|
||||||
|
n: 3
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const appliedObj = apply(oldObj, diffs);
|
||||||
|
expect(appliedObj).toEqual(newObj);
|
||||||
|
|
||||||
|
const revertedObj = revert(newObj, diffs);
|
||||||
|
expect(revertedObj).toEqual(oldObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle removing elements from an array", () => {
|
||||||
|
const oldObj = { a: [1, 2, 3] };
|
||||||
|
const newObj = { a: [1, 3] };
|
||||||
|
|
||||||
|
const diffs = diff(oldObj, newObj);
|
||||||
|
|
||||||
|
expect(diffs).toEqual([
|
||||||
|
{
|
||||||
|
t: "e",
|
||||||
|
p: ["a", 1],
|
||||||
|
o: 2,
|
||||||
|
n: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
t: "r",
|
||||||
|
p: ["a", 2],
|
||||||
|
o: 3,
|
||||||
|
n: undefined
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const appliedObj = apply(oldObj, diffs);
|
||||||
|
expect(appliedObj).toEqual(newObj);
|
||||||
|
|
||||||
|
const revertedObj = revert(newObj, diffs);
|
||||||
|
expect(revertedObj).toEqual(oldObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle complex nested changes", () => {
|
||||||
|
const oldObj = {
|
||||||
|
a: {
|
||||||
|
b: [1, 2, { c: 3 }]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const newObj = {
|
||||||
|
a: {
|
||||||
|
b: [1, 2, { c: 4 }, 5]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const diffs = diff(oldObj, newObj);
|
||||||
|
|
||||||
|
expect(diffs).toEqual([
|
||||||
|
{
|
||||||
|
t: "e",
|
||||||
|
p: ["a", "b", 2, "c"],
|
||||||
|
o: 3,
|
||||||
|
n: 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
t: "a",
|
||||||
|
p: ["a", "b", 3],
|
||||||
|
o: undefined,
|
||||||
|
n: 5
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const appliedObj = apply(oldObj, diffs);
|
||||||
|
expect(appliedObj).toEqual(newObj);
|
||||||
|
|
||||||
|
const revertedObj = revert(newObj, diffs);
|
||||||
|
expect(revertedObj).toEqual(oldObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle undefined and null values", () => {
|
||||||
|
const oldObj = { a: undefined, b: null };
|
||||||
|
const newObj = { a: null, b: undefined };
|
||||||
|
|
||||||
|
const diffs = diff(oldObj, newObj);
|
||||||
|
|
||||||
|
expect(diffs).toEqual([
|
||||||
|
{
|
||||||
|
t: "e",
|
||||||
|
p: ["a"],
|
||||||
|
o: undefined,
|
||||||
|
n: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
t: "e",
|
||||||
|
p: ["b"],
|
||||||
|
o: null,
|
||||||
|
n: undefined
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const appliedObj = apply(oldObj, diffs);
|
||||||
|
expect(appliedObj).toEqual(newObj);
|
||||||
|
|
||||||
|
const revertedObj = revert(newObj, diffs);
|
||||||
|
expect(revertedObj).toEqual(oldObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle type changes", () => {
|
||||||
|
const oldObj = { a: 1 };
|
||||||
|
const newObj = { a: "1" };
|
||||||
|
|
||||||
|
const diffs = diff(oldObj, newObj);
|
||||||
|
|
||||||
|
expect(diffs).toEqual([
|
||||||
|
{
|
||||||
|
t: "e",
|
||||||
|
p: ["a"],
|
||||||
|
o: 1,
|
||||||
|
n: "1"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const appliedObj = apply(oldObj, diffs);
|
||||||
|
expect(appliedObj).toEqual(newObj);
|
||||||
|
|
||||||
|
const revertedObj = revert(newObj, diffs);
|
||||||
|
expect(revertedObj).toEqual(oldObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle properties added and removed simultaneously", () => {
|
||||||
|
const oldObj = { a: 1, b: 2 };
|
||||||
|
const newObj = { a: 1, c: 3 };
|
||||||
|
|
||||||
|
const diffs = diff(oldObj, newObj);
|
||||||
|
|
||||||
|
expect(diffs).toEqual([
|
||||||
|
{
|
||||||
|
t: "r",
|
||||||
|
p: ["b"],
|
||||||
|
o: 2,
|
||||||
|
n: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
t: "a",
|
||||||
|
p: ["c"],
|
||||||
|
o: undefined,
|
||||||
|
n: 3
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const appliedObj = apply(oldObj, diffs);
|
||||||
|
expect(appliedObj).toEqual(newObj);
|
||||||
|
|
||||||
|
const revertedObj = revert(newObj, diffs);
|
||||||
|
expect(revertedObj).toEqual(oldObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle arrays replaced with objects", () => {
|
||||||
|
const oldObj = { a: [1, 2, 3] };
|
||||||
|
const newObj = { a: { b: 4 } };
|
||||||
|
|
||||||
|
const diffs = diff(oldObj, newObj);
|
||||||
|
|
||||||
|
expect(diffs).toEqual([
|
||||||
|
{
|
||||||
|
t: "e",
|
||||||
|
p: ["a"],
|
||||||
|
o: [1, 2, 3],
|
||||||
|
n: { b: 4 }
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const appliedObj = apply(oldObj, diffs);
|
||||||
|
expect(appliedObj).toEqual(newObj);
|
||||||
|
|
||||||
|
const revertedObj = revert(newObj, diffs);
|
||||||
|
expect(revertedObj).toEqual(oldObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle objects replaced with primitives", () => {
|
||||||
|
const oldObj = { a: { b: 1 } };
|
||||||
|
const newObj = { a: 2 };
|
||||||
|
|
||||||
|
const diffs = diff(oldObj, newObj);
|
||||||
|
|
||||||
|
expect(diffs).toEqual([
|
||||||
|
{
|
||||||
|
t: "e",
|
||||||
|
p: ["a"],
|
||||||
|
o: { b: 1 },
|
||||||
|
n: 2
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const appliedObj = apply(oldObj, diffs);
|
||||||
|
expect(appliedObj).toEqual(newObj);
|
||||||
|
|
||||||
|
const revertedObj = revert(newObj, diffs);
|
||||||
|
expect(revertedObj).toEqual(oldObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle root object changes", () => {
|
||||||
|
const oldObj = { a: 1 };
|
||||||
|
const newObj = { b: 2 };
|
||||||
|
|
||||||
|
const diffs = diff(oldObj, newObj);
|
||||||
|
|
||||||
|
expect(diffs).toEqual([
|
||||||
|
{
|
||||||
|
t: "r",
|
||||||
|
p: ["a"],
|
||||||
|
o: 1,
|
||||||
|
n: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
t: "a",
|
||||||
|
p: ["b"],
|
||||||
|
o: undefined,
|
||||||
|
n: 2
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const appliedObj = apply(oldObj, diffs);
|
||||||
|
expect(appliedObj).toEqual(newObj);
|
||||||
|
|
||||||
|
const revertedObj = revert(newObj, diffs);
|
||||||
|
expect(revertedObj).toEqual(oldObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle identical objects", () => {
|
||||||
|
const oldObj = { a: 1, b: { c: 2 } };
|
||||||
|
const newObj = { a: 1, b: { c: 2 } };
|
||||||
|
|
||||||
|
const diffs = diff(oldObj, newObj);
|
||||||
|
|
||||||
|
expect(diffs).toEqual([]);
|
||||||
|
|
||||||
|
const appliedObj = apply(oldObj, diffs);
|
||||||
|
expect(appliedObj).toEqual(newObj);
|
||||||
|
|
||||||
|
const revertedObj = revert(newObj, diffs);
|
||||||
|
expect(revertedObj).toEqual(oldObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle empty objects", () => {
|
||||||
|
const oldObj = {};
|
||||||
|
const newObj = {};
|
||||||
|
|
||||||
|
const diffs = diff(oldObj, newObj);
|
||||||
|
|
||||||
|
expect(diffs).toEqual([]);
|
||||||
|
|
||||||
|
const appliedObj = apply(oldObj, diffs);
|
||||||
|
expect(appliedObj).toEqual(newObj);
|
||||||
|
|
||||||
|
const revertedObj = revert(newObj, diffs);
|
||||||
|
expect(revertedObj).toEqual(oldObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle changes from empty object to non-empty object", () => {
|
||||||
|
const oldObj = {};
|
||||||
|
const newObj = { a: 1 };
|
||||||
|
|
||||||
|
const diffs = diff(oldObj, newObj);
|
||||||
|
|
||||||
|
expect(diffs).toEqual([
|
||||||
|
{
|
||||||
|
t: "a",
|
||||||
|
p: ["a"],
|
||||||
|
o: undefined,
|
||||||
|
n: 1
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const appliedObj = apply(oldObj, diffs);
|
||||||
|
expect(appliedObj).toEqual(newObj);
|
||||||
|
|
||||||
|
const revertedObj = revert(newObj, diffs);
|
||||||
|
expect(revertedObj).toEqual(oldObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle changes from non-empty object to empty object", () => {
|
||||||
|
const oldObj = { a: 1 };
|
||||||
|
const newObj = {};
|
||||||
|
|
||||||
|
const diffs = diff(oldObj, newObj);
|
||||||
|
|
||||||
|
expect(diffs).toEqual([
|
||||||
|
{
|
||||||
|
t: "r",
|
||||||
|
p: ["a"],
|
||||||
|
o: 1,
|
||||||
|
n: undefined
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const appliedObj = apply(oldObj, diffs);
|
||||||
|
expect(appliedObj).toEqual(newObj);
|
||||||
|
|
||||||
|
const revertedObj = revert(newObj, diffs);
|
||||||
|
expect(revertedObj).toEqual(oldObj);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { type ObjectQuery, convert, validate } from "../../../src/core/object/query/object-query";
|
import { type ObjectQuery, convert, validate } from "../../../src/core/object/query/object-query";
|
||||||
import { deprecated__whereRepoSchema } from "../../../src/data";
|
|
||||||
|
|
||||||
describe("object-query", () => {
|
describe("object-query", () => {
|
||||||
const q: ObjectQuery = { name: "Michael" };
|
const q: ObjectQuery = { name: "Michael" };
|
||||||
@@ -8,19 +7,6 @@ describe("object-query", () => {
|
|||||||
const q3: ObjectQuery = { name: "Michael", age: { $gt: 18 } };
|
const q3: ObjectQuery = { name: "Michael", age: { $gt: 18 } };
|
||||||
const bag = { q, q2, q3 };
|
const bag = { q, q2, q3 };
|
||||||
|
|
||||||
test("translates into legacy", async () => {
|
|
||||||
for (const [key, value] of Object.entries(bag)) {
|
|
||||||
const obj = convert(value);
|
|
||||||
try {
|
|
||||||
const parsed = deprecated__whereRepoSchema.parse(obj);
|
|
||||||
expect(parsed).toBeDefined();
|
|
||||||
} catch (e) {
|
|
||||||
console.log("errored", { obj, value });
|
|
||||||
console.error(key, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("validates", async () => {
|
test("validates", async () => {
|
||||||
const converted = convert({
|
const converted = convert({
|
||||||
name: { $eq: "ch" }
|
name: { $eq: "ch" }
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ describe("Mutator simple", async () => {
|
|||||||
new TextField("label", { required: true, minLength: 1 }),
|
new TextField("label", { required: true, minLength: 1 }),
|
||||||
new NumberField("count", { default_value: 0 })
|
new NumberField("count", { default_value: 0 })
|
||||||
]);
|
]);
|
||||||
const em = new EntityManager([items], connection);
|
const em = new EntityManager<any>([items], connection);
|
||||||
|
|
||||||
await em.connection.kysely.schema
|
await em.connection.kysely.schema
|
||||||
.createTable("items")
|
.createTable("items")
|
||||||
@@ -132,14 +132,61 @@ describe("Mutator simple", async () => {
|
|||||||
const data = (await em.repository(items).findMany()).data;
|
const data = (await em.repository(items).findMany()).data;
|
||||||
//console.log(data);
|
//console.log(data);
|
||||||
|
|
||||||
await em.mutator(items).deleteMany({ label: "delete" });
|
await em.mutator(items).deleteWhere({ label: "delete" });
|
||||||
|
|
||||||
expect((await em.repository(items).findMany()).data.length).toBe(data.length - 2);
|
expect((await em.repository(items).findMany()).data.length).toBe(data.length - 2);
|
||||||
//console.log((await em.repository(items).findMany()).data);
|
//console.log((await em.repository(items).findMany()).data);
|
||||||
|
|
||||||
await em.mutator(items).deleteMany();
|
await em.mutator(items).deleteWhere();
|
||||||
expect((await em.repository(items).findMany()).data.length).toBe(0);
|
expect((await em.repository(items).findMany()).data.length).toBe(0);
|
||||||
|
|
||||||
//expect(res.data.count).toBe(0);
|
//expect(res.data.count).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("updateMany", async () => {
|
||||||
|
await em.mutator(items).insertOne({ label: "update", count: 1 });
|
||||||
|
await em.mutator(items).insertOne({ label: "update too", count: 1 });
|
||||||
|
await em.mutator(items).insertOne({ label: "keep" });
|
||||||
|
|
||||||
|
// expect no update
|
||||||
|
await em.mutator(items).updateWhere(
|
||||||
|
{ count: 2 },
|
||||||
|
{
|
||||||
|
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 }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// expect 2 to be updated
|
||||||
|
await em.mutator(items).updateWhere(
|
||||||
|
{ count: 2 },
|
||||||
|
{
|
||||||
|
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 }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("insertMany", async () => {
|
||||||
|
const oldCount = (await em.repo(items).count()).count;
|
||||||
|
const inserts = [{ label: "insert 1" }, { label: "insert 2" }];
|
||||||
|
const { data } = await em.mutator(items).insertMany(inserts);
|
||||||
|
|
||||||
|
expect(data.length).toBe(2);
|
||||||
|
expect(data.map((d) => ({ label: d.label }))).toEqual(inserts);
|
||||||
|
const newCount = (await em.repo(items).count()).count;
|
||||||
|
expect(newCount).toBe(oldCount + inserts.length);
|
||||||
|
|
||||||
|
const { data: data2 } = await em.repo(items).findMany({ offset: oldCount });
|
||||||
|
expect(data2).toEqual(data);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { MediaField } from "../../src";
|
|
||||||
import {
|
import {
|
||||||
BooleanField,
|
BooleanField,
|
||||||
DateField,
|
DateField,
|
||||||
Entity,
|
Entity,
|
||||||
|
EntityIndex,
|
||||||
|
EntityManager,
|
||||||
EnumField,
|
EnumField,
|
||||||
JsonField,
|
JsonField,
|
||||||
ManyToManyRelation,
|
ManyToManyRelation,
|
||||||
@@ -13,6 +14,7 @@ import {
|
|||||||
PolymorphicRelation,
|
PolymorphicRelation,
|
||||||
TextField
|
TextField
|
||||||
} from "../../src/data";
|
} from "../../src/data";
|
||||||
|
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
||||||
import {
|
import {
|
||||||
FieldPrototype,
|
FieldPrototype,
|
||||||
type FieldSchema,
|
type FieldSchema,
|
||||||
@@ -21,6 +23,7 @@ import {
|
|||||||
boolean,
|
boolean,
|
||||||
date,
|
date,
|
||||||
datetime,
|
datetime,
|
||||||
|
em,
|
||||||
entity,
|
entity,
|
||||||
enumm,
|
enumm,
|
||||||
json,
|
json,
|
||||||
@@ -30,6 +33,7 @@ import {
|
|||||||
relation,
|
relation,
|
||||||
text
|
text
|
||||||
} from "../../src/data/prototype";
|
} from "../../src/data/prototype";
|
||||||
|
import { MediaField } from "../../src/media/MediaField";
|
||||||
|
|
||||||
describe("prototype", () => {
|
describe("prototype", () => {
|
||||||
test("...", () => {
|
test("...", () => {
|
||||||
@@ -46,12 +50,17 @@ describe("prototype", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("...2", async () => {
|
test("...2", async () => {
|
||||||
const user = entity("users", {
|
const users = entity("users", {
|
||||||
name: text().required(),
|
name: text(),
|
||||||
bio: text(),
|
bio: text(),
|
||||||
age: number(),
|
age: number(),
|
||||||
some: number().required()
|
some: number()
|
||||||
});
|
});
|
||||||
|
type db = {
|
||||||
|
users: Schema<typeof users>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const obj: Schema<typeof users> = {} as any;
|
||||||
|
|
||||||
//console.log("user", user.toJSON());
|
//console.log("user", user.toJSON());
|
||||||
});
|
});
|
||||||
@@ -76,7 +85,9 @@ describe("prototype", () => {
|
|||||||
new DateField("created_at", {
|
new DateField("created_at", {
|
||||||
type: "datetime"
|
type: "datetime"
|
||||||
}),
|
}),
|
||||||
|
// @ts-ignore
|
||||||
new MediaField("images", { entity: "posts" }),
|
new MediaField("images", { entity: "posts" }),
|
||||||
|
// @ts-ignore
|
||||||
new MediaField("cover", { entity: "posts", max_items: 1 })
|
new MediaField("cover", { entity: "posts", max_items: 1 })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -264,4 +275,38 @@ describe("prototype", () => {
|
|||||||
|
|
||||||
const obj: Schema<typeof test> = {} as any;
|
const obj: Schema<typeof test> = {} as any;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("schema", async () => {
|
||||||
|
const _em = em(
|
||||||
|
{
|
||||||
|
posts: entity("posts", { name: text(), slug: text().required() }),
|
||||||
|
comments: entity("comments", { some: 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"];
|
||||||
|
|
||||||
|
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")])
|
||||||
|
];
|
||||||
|
const _em2 = new EntityManager(
|
||||||
|
es,
|
||||||
|
new DummyConnection(),
|
||||||
|
[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)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
expect(_em2.toJSON()).toEqual(_em.toJSON());
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ describe("[data] Mutator (base)", async () => {
|
|||||||
new TextField("hidden", { hidden: true }),
|
new TextField("hidden", { hidden: true }),
|
||||||
new TextField("not_fillable", { fillable: false })
|
new TextField("not_fillable", { fillable: false })
|
||||||
]);
|
]);
|
||||||
const em = new EntityManager([entity], dummyConnection);
|
const em = new EntityManager<any>([entity], dummyConnection);
|
||||||
await em.schema().sync({ force: true });
|
await em.schema().sync({ force: true });
|
||||||
|
|
||||||
const payload = { label: "item 1", count: 1 };
|
const payload = { label: "item 1", count: 1 };
|
||||||
@@ -61,7 +61,7 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
|||||||
const posts = new Entity("posts", [new TextField("title")]);
|
const posts = new Entity("posts", [new TextField("title")]);
|
||||||
const users = new Entity("users", [new TextField("username")]);
|
const users = new Entity("users", [new TextField("username")]);
|
||||||
const relations = [new ManyToOneRelation(posts, users)];
|
const relations = [new ManyToOneRelation(posts, users)];
|
||||||
const em = new EntityManager([posts, users], dummyConnection, relations);
|
const em = new EntityManager<any>([posts, users], dummyConnection, relations);
|
||||||
await em.schema().sync({ force: true });
|
await em.schema().sync({ force: true });
|
||||||
|
|
||||||
test("RelationMutator", async () => {
|
test("RelationMutator", async () => {
|
||||||
@@ -192,7 +192,7 @@ describe("[data] Mutator (OneToOne)", async () => {
|
|||||||
const users = new Entity("users", [new TextField("username")]);
|
const users = new Entity("users", [new TextField("username")]);
|
||||||
const settings = new Entity("settings", [new TextField("theme")]);
|
const settings = new Entity("settings", [new TextField("theme")]);
|
||||||
const relations = [new OneToOneRelation(users, settings)];
|
const relations = [new OneToOneRelation(users, settings)];
|
||||||
const em = new EntityManager([users, settings], dummyConnection, relations);
|
const em = new EntityManager<any>([users, settings], dummyConnection, relations);
|
||||||
await em.schema().sync({ force: true });
|
await em.schema().sync({ force: true });
|
||||||
|
|
||||||
test("insertOne: missing ref", async () => {
|
test("insertOne: missing ref", async () => {
|
||||||
@@ -276,7 +276,7 @@ describe("[data] Mutator (ManyToMany)", async () => {
|
|||||||
|
|
||||||
describe("[data] Mutator (Events)", async () => {
|
describe("[data] Mutator (Events)", async () => {
|
||||||
const entity = new Entity("test", [new TextField("label")]);
|
const entity = new Entity("test", [new TextField("label")]);
|
||||||
const em = new EntityManager([entity], dummyConnection);
|
const em = new EntityManager<any>([entity], dummyConnection);
|
||||||
await em.schema().sync({ force: true });
|
await em.schema().sync({ force: true });
|
||||||
const events = new Map<string, any>();
|
const events = new Map<string, any>();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
describe("media/mime-types", () => {
|
||||||
|
test("tiny resolves", () => {
|
||||||
|
const tests = [[".mp4", "video/mp4", ".jpg", "image/jpeg", ".zip", "application/zip"]];
|
||||||
|
|
||||||
|
for (const [ext, mime] of tests) {
|
||||||
|
expect(tiny.guess(ext)).toBe(mime);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("all tiny resolves to large", () => {
|
||||||
|
for (const [ext, mime] of Object.entries(tiny.M)) {
|
||||||
|
expect(large.guessMimeType("." + ext)).toBe(mime);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [type, exts] of Object.entries(tiny.Q)) {
|
||||||
|
for (const ext of exts) {
|
||||||
|
const ex = `${type}/${ext}`;
|
||||||
|
try {
|
||||||
|
expect(large.guessMimeType("." + ext)).toBe(ex);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`Failed for ${ext}`, {
|
||||||
|
type,
|
||||||
|
exts,
|
||||||
|
ext,
|
||||||
|
expected: ex,
|
||||||
|
actual: large.guessMimeType("." + ext)
|
||||||
|
});
|
||||||
|
throw new Error(`Failed for ${ext}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -39,7 +39,10 @@ describe("AppAuth", () => {
|
|||||||
test("creates user on register", async () => {
|
test("creates user on register", async () => {
|
||||||
const auth = new AppAuth(
|
const auth = new AppAuth(
|
||||||
{
|
{
|
||||||
enabled: true
|
enabled: true,
|
||||||
|
jwt: {
|
||||||
|
secret: "123456"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
ctx
|
ctx
|
||||||
);
|
);
|
||||||
@@ -57,6 +60,9 @@ describe("AppAuth", () => {
|
|||||||
disableConsoleLog();
|
disableConsoleLog();
|
||||||
const res = await app.request("/password/register", {
|
const res = await app.request("/password/register", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
email: "some@body.com",
|
email: "some@body.com",
|
||||||
password: "123456"
|
password: "123456"
|
||||||
|
|||||||
+52
-9
@@ -9,16 +9,44 @@ const watch = args.includes("--watch");
|
|||||||
const minify = args.includes("--minify");
|
const minify = args.includes("--minify");
|
||||||
const types = args.includes("--types");
|
const types = args.includes("--types");
|
||||||
const sourcemap = args.includes("--sourcemap");
|
const sourcemap = args.includes("--sourcemap");
|
||||||
|
const clean = args.includes("--clean");
|
||||||
|
|
||||||
|
if (clean) {
|
||||||
|
console.log("Cleaning dist");
|
||||||
|
await $`rm -rf dist`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let types_running = false;
|
||||||
|
function buildTypes() {
|
||||||
|
if (types_running) return;
|
||||||
|
types_running = true;
|
||||||
|
|
||||||
await $`rm -rf dist`;
|
|
||||||
if (types) {
|
|
||||||
Bun.spawn(["bun", "build:types"], {
|
Bun.spawn(["bun", "build:types"], {
|
||||||
onExit: () => {
|
onExit: () => {
|
||||||
console.log("Types built");
|
console.log("Types built");
|
||||||
|
Bun.spawn(["bun", "tsc-alias"], {
|
||||||
|
onExit: () => {
|
||||||
|
console.log("Types aliased");
|
||||||
|
types_running = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let watcher_timeout: any;
|
||||||
|
function delayTypes() {
|
||||||
|
if (!watch) return;
|
||||||
|
if (watcher_timeout) {
|
||||||
|
clearTimeout(watcher_timeout);
|
||||||
|
}
|
||||||
|
watcher_timeout = setTimeout(buildTypes, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (types && !watch) {
|
||||||
|
buildTypes();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build static assets
|
* Build static assets
|
||||||
* Using esbuild because tsup doesn't include "react"
|
* Using esbuild because tsup doesn't include "react"
|
||||||
@@ -46,7 +74,8 @@ const result = await esbuild.build({
|
|||||||
__isDev: "0",
|
__isDev: "0",
|
||||||
"process.env.NODE_ENV": '"production"'
|
"process.env.NODE_ENV": '"production"'
|
||||||
},
|
},
|
||||||
chunkNames: "chunks/[name]-[hash]"
|
chunkNames: "chunks/[name]-[hash]",
|
||||||
|
logLevel: "error"
|
||||||
});
|
});
|
||||||
|
|
||||||
// Write manifest
|
// Write manifest
|
||||||
@@ -88,13 +117,17 @@ await tsup.build({
|
|||||||
watch,
|
watch,
|
||||||
entry: ["src/index.ts", "src/data/index.ts", "src/core/index.ts", "src/core/utils/index.ts"],
|
entry: ["src/index.ts", "src/data/index.ts", "src/core/index.ts", "src/core/utils/index.ts"],
|
||||||
outDir: "dist",
|
outDir: "dist",
|
||||||
external: ["bun:test"],
|
external: ["bun:test", "@libsql/client"],
|
||||||
metafile: true,
|
metafile: true,
|
||||||
platform: "browser",
|
platform: "browser",
|
||||||
format: ["esm", "cjs"],
|
format: ["esm", "cjs"],
|
||||||
splitting: false,
|
splitting: false,
|
||||||
|
treeshake: true,
|
||||||
loader: {
|
loader: {
|
||||||
".svg": "dataurl"
|
".svg": "dataurl"
|
||||||
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
delayTypes();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -107,19 +140,21 @@ await tsup.build({
|
|||||||
watch,
|
watch,
|
||||||
entry: ["src/ui/index.ts", "src/ui/client/index.ts", "src/ui/main.css"],
|
entry: ["src/ui/index.ts", "src/ui/client/index.ts", "src/ui/main.css"],
|
||||||
outDir: "dist/ui",
|
outDir: "dist/ui",
|
||||||
external: ["bun:test"],
|
external: ["bun:test", "react", "react-dom", "use-sync-external-store"],
|
||||||
metafile: true,
|
metafile: true,
|
||||||
platform: "browser",
|
platform: "browser",
|
||||||
format: ["esm", "cjs"],
|
format: ["esm", "cjs"],
|
||||||
splitting: true,
|
splitting: true,
|
||||||
|
treeshake: true,
|
||||||
loader: {
|
loader: {
|
||||||
".svg": "dataurl"
|
".svg": "dataurl"
|
||||||
},
|
},
|
||||||
onSuccess: async () => {
|
|
||||||
console.log("--- ui built");
|
|
||||||
},
|
|
||||||
esbuildOptions: (options) => {
|
esbuildOptions: (options) => {
|
||||||
|
options.logLevel = "silent";
|
||||||
options.chunkNames = "chunks/[name]-[hash]";
|
options.chunkNames = "chunks/[name]-[hash]";
|
||||||
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
delayTypes();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -146,7 +181,10 @@ function baseConfig(adapter: string): tsup.Options {
|
|||||||
],
|
],
|
||||||
metafile: true,
|
metafile: true,
|
||||||
splitting: false,
|
splitting: false,
|
||||||
treeshake: true
|
treeshake: true,
|
||||||
|
onSuccess: async () => {
|
||||||
|
delayTypes();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,3 +217,8 @@ await tsup.build({
|
|||||||
platform: "node",
|
platform: "node",
|
||||||
format: ["esm", "cjs"]
|
format: ["esm", "cjs"]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await tsup.build({
|
||||||
|
...baseConfig("astro"),
|
||||||
|
format: ["esm", "cjs"]
|
||||||
|
});
|
||||||
|
|||||||
+61
-54
@@ -3,34 +3,50 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"bin": "./dist/cli/index.js",
|
"bin": "./dist/cli/index.js",
|
||||||
"version": "0.1.0",
|
"version": "0.4.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:all": "bun run build && bun run build:cli",
|
"build:all": "NODE_ENV=production bun run build.ts --minify --types --clean && bun run build:cli",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"test": "ALL_TESTS=1 bun test --bail",
|
"test": "ALL_TESTS=1 bun test --bail",
|
||||||
"build": "bun run build.ts --minify --types",
|
"build": "NODE_ENV=production bun run build.ts --minify --types",
|
||||||
"watch": "bun run build.ts --types --watch",
|
"watch": "bun run build.ts --types --watch",
|
||||||
"types": "bun tsc --noEmit",
|
"types": "bun tsc --noEmit",
|
||||||
"clean:types": "find ./dist -name '*.d.ts' -delete && rm -f ./dist/tsconfig.tsbuildinfo",
|
"clean:types": "find ./dist -name '*.d.ts' -delete && rm -f ./dist/tsconfig.tsbuildinfo",
|
||||||
"build:types": "tsc --emitDeclarationOnly",
|
"build:types": "tsc --emitDeclarationOnly && tsc-alias",
|
||||||
"build:css": "bun tailwindcss -i src/ui/main.css -o ./dist/static/styles.css",
|
"build:css": "bun tailwindcss -i src/ui/main.css -o ./dist/static/styles.css",
|
||||||
"watch:css": "bun tailwindcss --watch -i src/ui/main.css -o ./dist/styles.css",
|
"watch:css": "bun tailwindcss --watch -i src/ui/main.css -o ./dist/styles.css",
|
||||||
"updater": "bun x npm-check-updates -ui",
|
"updater": "bun x npm-check-updates -ui",
|
||||||
"build:cli": "bun build src/cli/index.ts --target node --outdir dist/cli --minify",
|
"build:cli": "bun build src/cli/index.ts --target node --outdir dist/cli --minify",
|
||||||
"cli": "LOCAL=1 bun src/cli/index.ts"
|
"cli": "LOCAL=1 bun src/cli/index.ts",
|
||||||
|
"prepublishOnly": "bun run build:all"
|
||||||
},
|
},
|
||||||
"license": "FSL-1.1-MIT",
|
"license": "FSL-1.1-MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cfworker/json-schema": "^2.0.1",
|
"@cfworker/json-schema": "^2.0.1",
|
||||||
|
"@libsql/client": "^0.14.0",
|
||||||
|
"@sinclair/typebox": "^0.32.34",
|
||||||
|
"@tanstack/react-form": "0.19.2",
|
||||||
|
"aws4fetch": "^1.0.18",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
|
"fast-xml-parser": "^4.4.0",
|
||||||
|
"hono": "^4.6.12",
|
||||||
|
"kysely": "^0.27.4",
|
||||||
|
"liquidjs": "^10.15.0",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
|
"oauth4webapi": "^2.11.1",
|
||||||
|
"swr": "^2.2.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.613.0",
|
||||||
"@codemirror/lang-html": "^6.4.9",
|
"@codemirror/lang-html": "^6.4.9",
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@codemirror/lang-liquid": "^6.2.1",
|
"@codemirror/lang-liquid": "^6.2.1",
|
||||||
"@dagrejs/dagre": "^1.1.4",
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
"@hello-pangea/dnd": "^17.0.0",
|
"@hello-pangea/dnd": "^17.0.0",
|
||||||
"@hono/typebox-validator": "^0.2.6",
|
"@hono/typebox-validator": "^0.2.6",
|
||||||
|
"@hono/vite-dev-server": "^0.17.0",
|
||||||
"@hono/zod-validator": "^0.4.1",
|
"@hono/zod-validator": "^0.4.1",
|
||||||
"@hookform/resolvers": "^3.9.1",
|
"@hookform/resolvers": "^3.9.1",
|
||||||
"@libsql/client": "^0.14.0",
|
|
||||||
"@libsql/kysely-libsql": "^0.4.1",
|
"@libsql/kysely-libsql": "^0.4.1",
|
||||||
"@mantine/core": "^7.13.4",
|
"@mantine/core": "^7.13.4",
|
||||||
"@mantine/hooks": "^7.13.4",
|
"@mantine/hooks": "^7.13.4",
|
||||||
@@ -38,54 +54,36 @@
|
|||||||
"@mantine/notifications": "^7.13.5",
|
"@mantine/notifications": "^7.13.5",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.0",
|
"@radix-ui/react-scroll-area": "^1.2.0",
|
||||||
"@rjsf/core": "^5.22.2",
|
"@rjsf/core": "^5.22.2",
|
||||||
"@sinclair/typebox": "^0.32.34",
|
|
||||||
"@tabler/icons-react": "3.18.0",
|
"@tabler/icons-react": "3.18.0",
|
||||||
"@tanstack/react-form": "0.19.2",
|
|
||||||
"@tanstack/react-query": "^5.59.16",
|
|
||||||
"@uiw/react-codemirror": "^4.23.6",
|
|
||||||
"@xyflow/react": "^12.3.2",
|
|
||||||
"aws4fetch": "^1.0.18",
|
|
||||||
"codemirror-lang-liquid": "^1.0.0",
|
|
||||||
"dayjs": "^1.11.13",
|
|
||||||
"fast-xml-parser": "^4.4.0",
|
|
||||||
"hono": "^4.6.12",
|
|
||||||
"jotai": "^2.10.1",
|
|
||||||
"kysely": "^0.27.4",
|
|
||||||
"liquidjs": "^10.15.0",
|
|
||||||
"lodash-es": "^4.17.21",
|
|
||||||
"oauth4webapi": "^2.11.1",
|
|
||||||
"react-hook-form": "^7.53.1",
|
|
||||||
"react-icons": "5.2.1",
|
|
||||||
"react-json-view-lite": "^2.0.1",
|
|
||||||
"reactflow": "^11.11.4",
|
|
||||||
"tailwind-merge": "^2.5.4",
|
|
||||||
"tailwindcss-animate": "^1.0.7",
|
|
||||||
"wouter": "^3.3.5",
|
|
||||||
"zod": "^3.23.8",
|
|
||||||
"zod-to-json-schema": "^3.23.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@aws-sdk/client-s3": "^3.613.0",
|
|
||||||
"@hono/node-server": "^1.13.7",
|
|
||||||
"@hono/vite-dev-server": "^0.17.0",
|
|
||||||
"@tanstack/react-query-devtools": "^5.59.16",
|
|
||||||
"@types/diff": "^5.2.3",
|
|
||||||
"@types/node": "^22.10.0",
|
"@types/node": "^22.10.0",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
|
"@uiw/react-codemirror": "^4.23.6",
|
||||||
"@vitejs/plugin-react": "^4.3.3",
|
"@vitejs/plugin-react": "^4.3.3",
|
||||||
|
"@xyflow/react": "^12.3.2",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"esbuild-postcss": "^0.0.4",
|
"esbuild-postcss": "^0.0.4",
|
||||||
"node-fetch": "^3.3.2",
|
"jotai": "^2.10.1",
|
||||||
|
"open": "^10.1.0",
|
||||||
"openapi-types": "^12.1.3",
|
"openapi-types": "^12.1.3",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"postcss-preset-mantine": "^1.17.0",
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
|
"react-hook-form": "^7.53.1",
|
||||||
|
"react-icons": "5.2.1",
|
||||||
|
"react-json-view-lite": "^2.0.1",
|
||||||
|
"tailwind-merge": "^2.5.4",
|
||||||
"tailwindcss": "^3.4.14",
|
"tailwindcss": "^3.4.14",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"tsc-alias": "^1.8.10",
|
||||||
"tsup": "^8.3.5",
|
"tsup": "^8.3.5",
|
||||||
"vite": "^5.4.10",
|
"vite": "^5.4.10",
|
||||||
"vite-plugin-static-copy": "^2.0.0",
|
"vite-plugin-static-copy": "^2.0.0",
|
||||||
"vite-tsconfig-paths": "^5.0.1"
|
"vite-tsconfig-paths": "^5.0.1",
|
||||||
|
"wouter": "^3.3.5"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@hono/node-server": "^1.13.7"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": ">=18",
|
"react": ">=18",
|
||||||
@@ -93,82 +91,91 @@
|
|||||||
},
|
},
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"module": "./dist/index.js",
|
"module": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/types/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/types/index.d.ts",
|
||||||
"import": "./dist/index.js",
|
"import": "./dist/index.js",
|
||||||
"require": "./dist/index.cjs"
|
"require": "./dist/index.cjs"
|
||||||
},
|
},
|
||||||
"./ui": {
|
"./ui": {
|
||||||
"types": "./dist/ui/index.d.ts",
|
"types": "./dist/types/ui/index.d.ts",
|
||||||
"import": "./dist/ui/index.js",
|
"import": "./dist/ui/index.js",
|
||||||
"require": "./dist/ui/index.cjs"
|
"require": "./dist/ui/index.cjs"
|
||||||
},
|
},
|
||||||
"./client": {
|
"./client": {
|
||||||
"types": "./dist/ui/client/index.d.ts",
|
"types": "./dist/types/ui/client/index.d.ts",
|
||||||
"import": "./dist/ui/client/index.js",
|
"import": "./dist/ui/client/index.js",
|
||||||
"require": "./dist/ui/client/index.cjs"
|
"require": "./dist/ui/client/index.cjs"
|
||||||
},
|
},
|
||||||
"./data": {
|
"./data": {
|
||||||
"types": "./dist/data/index.d.ts",
|
"types": "./dist/types/data/index.d.ts",
|
||||||
"import": "./dist/data/index.js",
|
"import": "./dist/data/index.js",
|
||||||
"require": "./dist/data/index.cjs"
|
"require": "./dist/data/index.cjs"
|
||||||
},
|
},
|
||||||
"./core": {
|
"./core": {
|
||||||
"types": "./dist/core/index.d.ts",
|
"types": "./dist/types/core/index.d.ts",
|
||||||
"import": "./dist/core/index.js",
|
"import": "./dist/core/index.js",
|
||||||
"require": "./dist/core/index.cjs"
|
"require": "./dist/core/index.cjs"
|
||||||
},
|
},
|
||||||
"./utils": {
|
"./utils": {
|
||||||
"types": "./dist/core/utils/index.d.ts",
|
"types": "./dist/types/core/utils/index.d.ts",
|
||||||
"import": "./dist/core/utils/index.js",
|
"import": "./dist/core/utils/index.js",
|
||||||
"require": "./dist/core/utils/index.cjs"
|
"require": "./dist/core/utils/index.cjs"
|
||||||
},
|
},
|
||||||
"./cli": {
|
"./cli": {
|
||||||
"types": "./dist/cli/index.d.ts",
|
"types": "./dist/types/cli/index.d.ts",
|
||||||
"import": "./dist/cli/index.js",
|
"import": "./dist/cli/index.js",
|
||||||
"require": "./dist/cli/index.cjs"
|
"require": "./dist/cli/index.cjs"
|
||||||
},
|
},
|
||||||
"./adapter/cloudflare": {
|
"./adapter/cloudflare": {
|
||||||
"types": "./dist/adapter/cloudflare/index.d.ts",
|
"types": "./dist/types/adapter/cloudflare/index.d.ts",
|
||||||
"import": "./dist/adapter/cloudflare/index.js",
|
"import": "./dist/adapter/cloudflare/index.js",
|
||||||
"require": "./dist/adapter/cloudflare/index.cjs"
|
"require": "./dist/adapter/cloudflare/index.cjs"
|
||||||
},
|
},
|
||||||
"./adapter/vite": {
|
"./adapter/vite": {
|
||||||
"types": "./dist/adapter/vite/index.d.ts",
|
"types": "./dist/types/adapter/vite/index.d.ts",
|
||||||
"import": "./dist/adapter/vite/index.js",
|
"import": "./dist/adapter/vite/index.js",
|
||||||
"require": "./dist/adapter/vite/index.cjs"
|
"require": "./dist/adapter/vite/index.cjs"
|
||||||
},
|
},
|
||||||
"./adapter/nextjs": {
|
"./adapter/nextjs": {
|
||||||
"types": "./dist/adapter/nextjs/index.d.ts",
|
"types": "./dist/types/adapter/nextjs/index.d.ts",
|
||||||
"import": "./dist/adapter/nextjs/index.js",
|
"import": "./dist/adapter/nextjs/index.js",
|
||||||
"require": "./dist/adapter/nextjs/index.cjs"
|
"require": "./dist/adapter/nextjs/index.cjs"
|
||||||
},
|
},
|
||||||
"./adapter/remix": {
|
"./adapter/remix": {
|
||||||
"types": "./dist/adapter/remix/index.d.ts",
|
"types": "./dist/types/adapter/remix/index.d.ts",
|
||||||
"import": "./dist/adapter/remix/index.js",
|
"import": "./dist/adapter/remix/index.js",
|
||||||
"require": "./dist/adapter/remix/index.cjs"
|
"require": "./dist/adapter/remix/index.cjs"
|
||||||
},
|
},
|
||||||
"./adapter/bun": {
|
"./adapter/bun": {
|
||||||
"types": "./dist/adapter/bun/index.d.ts",
|
"types": "./dist/types/adapter/bun/index.d.ts",
|
||||||
"import": "./dist/adapter/bun/index.js",
|
"import": "./dist/adapter/bun/index.js",
|
||||||
"require": "./dist/adapter/bun/index.cjs"
|
"require": "./dist/adapter/bun/index.cjs"
|
||||||
},
|
},
|
||||||
"./adapter/node": {
|
"./adapter/node": {
|
||||||
"types": "./dist/adapter/node/index.d.ts",
|
"types": "./dist/types/adapter/node/index.d.ts",
|
||||||
"import": "./dist/adapter/node/index.js",
|
"import": "./dist/adapter/node/index.js",
|
||||||
"require": "./dist/adapter/node/index.cjs"
|
"require": "./dist/adapter/node/index.cjs"
|
||||||
},
|
},
|
||||||
|
"./adapter/astro": {
|
||||||
|
"types": "./dist/types/adapter/astro/index.d.ts",
|
||||||
|
"import": "./dist/adapter/astro/index.js",
|
||||||
|
"require": "./dist/adapter/astro/index.cjs"
|
||||||
|
},
|
||||||
"./dist/styles.css": "./dist/ui/main.css",
|
"./dist/styles.css": "./dist/ui/main.css",
|
||||||
"./dist/manifest.json": "./dist/static/manifest.json"
|
"./dist/manifest.json": "./dist/static/manifest.json"
|
||||||
},
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
"README.md",
|
"README.md",
|
||||||
"!dist/*.tsbuildinfo",
|
"!dist/*.tsbuildinfo",
|
||||||
"!dist/*.map",
|
"!dist/*.map",
|
||||||
"!dist/**/*.map",
|
"!dist/**/*.map",
|
||||||
"!dist/metafile*"
|
"!dist/metafile*",
|
||||||
|
"!dist/**/metafile*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-3
@@ -1,3 +1,4 @@
|
|||||||
|
import type { SafeUser } from "auth";
|
||||||
import { AuthApi } from "auth/api/AuthApi";
|
import { AuthApi } from "auth/api/AuthApi";
|
||||||
import { DataApi } from "data/api/DataApi";
|
import { DataApi } from "data/api/DataApi";
|
||||||
import { decode } from "hono/jwt";
|
import { decode } from "hono/jwt";
|
||||||
@@ -5,7 +6,7 @@ import { omit } from "lodash-es";
|
|||||||
import { MediaApi } from "media/api/MediaApi";
|
import { MediaApi } from "media/api/MediaApi";
|
||||||
import { SystemApi } from "modules/SystemApi";
|
import { SystemApi } from "modules/SystemApi";
|
||||||
|
|
||||||
export type TApiUser = object;
|
export type TApiUser = SafeUser;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -24,6 +25,12 @@ export type ApiOptions = {
|
|||||||
localStorage?: boolean;
|
localStorage?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AuthState = {
|
||||||
|
token?: string;
|
||||||
|
user?: TApiUser;
|
||||||
|
verified: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export class Api {
|
export class Api {
|
||||||
private token?: string;
|
private token?: string;
|
||||||
private user?: TApiUser;
|
private user?: TApiUser;
|
||||||
@@ -50,6 +57,10 @@ export class Api {
|
|||||||
this.buildApis();
|
this.buildApis();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get baseUrl() {
|
||||||
|
return this.options.host;
|
||||||
|
}
|
||||||
|
|
||||||
get tokenKey() {
|
get tokenKey() {
|
||||||
return this.options.key ?? "auth";
|
return this.options.key ?? "auth";
|
||||||
}
|
}
|
||||||
@@ -85,7 +96,11 @@ export class Api {
|
|||||||
|
|
||||||
updateToken(token?: string, rebuild?: boolean) {
|
updateToken(token?: string, rebuild?: boolean) {
|
||||||
this.token = token;
|
this.token = token;
|
||||||
this.user = token ? omit(decode(token).payload as any, ["iat", "iss", "exp"]) : undefined;
|
if (token) {
|
||||||
|
this.user = omit(decode(token).payload as any, ["iat", "iss", "exp"]) as any;
|
||||||
|
} else {
|
||||||
|
this.user = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.options.localStorage) {
|
if (this.options.localStorage) {
|
||||||
const key = this.tokenKey;
|
const key = this.tokenKey;
|
||||||
@@ -105,7 +120,7 @@ export class Api {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAuthState() {
|
getAuthState(): AuthState {
|
||||||
return {
|
return {
|
||||||
token: this.token,
|
token: this.token,
|
||||||
user: this.user,
|
user: this.user,
|
||||||
@@ -113,6 +128,28 @@ export class Api {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getVerifiedAuthState(force?: boolean): Promise<AuthState> {
|
||||||
|
if (force === true || !this.verified) {
|
||||||
|
await this.verifyAuth();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getAuthState();
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyAuth() {
|
||||||
|
try {
|
||||||
|
const res = await this.auth.me();
|
||||||
|
if (!res.ok || !res.body.user) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.markAuthVerified(true);
|
||||||
|
} catch (e) {
|
||||||
|
this.markAuthVerified(false);
|
||||||
|
this.updateToken(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getUser(): TApiUser | null {
|
getUser(): TApiUser | null {
|
||||||
return this.user || null;
|
return this.user || null;
|
||||||
}
|
}
|
||||||
|
|||||||
+68
-34
@@ -10,73 +10,69 @@ import * as SystemPermissions from "modules/permissions";
|
|||||||
import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
|
import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
|
||||||
import { SystemController } from "modules/server/SystemController";
|
import { SystemController } from "modules/server/SystemController";
|
||||||
|
|
||||||
export type AppPlugin<DB> = (app: App<DB>) => void;
|
export type AppPlugin = (app: App) => Promise<void> | void;
|
||||||
|
|
||||||
export class AppConfigUpdatedEvent extends Event<{ app: App }> {
|
abstract class AppEvent<A = {}> extends Event<{ app: App } & A> {}
|
||||||
|
export class AppConfigUpdatedEvent extends AppEvent {
|
||||||
static override slug = "app-config-updated";
|
static override slug = "app-config-updated";
|
||||||
}
|
}
|
||||||
export class AppBuiltEvent extends Event<{ app: App }> {
|
export class AppBuiltEvent extends AppEvent {
|
||||||
static override slug = "app-built";
|
static override slug = "app-built";
|
||||||
}
|
}
|
||||||
export const AppEvents = { AppConfigUpdatedEvent, AppBuiltEvent } as const;
|
export class AppFirstBoot extends AppEvent {
|
||||||
|
static override slug = "app-first-boot";
|
||||||
|
}
|
||||||
|
export const AppEvents = { AppConfigUpdatedEvent, AppBuiltEvent, AppFirstBoot } as const;
|
||||||
|
|
||||||
export type CreateAppConfig = {
|
export type CreateAppConfig = {
|
||||||
connection:
|
connection?:
|
||||||
| Connection
|
| Connection
|
||||||
| {
|
| {
|
||||||
type: "libsql";
|
type: "libsql";
|
||||||
config: LibSqlCredentials;
|
config: LibSqlCredentials;
|
||||||
};
|
};
|
||||||
initialConfig?: InitialModuleConfigs;
|
initialConfig?: InitialModuleConfigs;
|
||||||
plugins?: AppPlugin<any>[];
|
plugins?: AppPlugin[];
|
||||||
options?: ModuleManagerOptions;
|
options?: Omit<ModuleManagerOptions, "initial" | "onUpdated">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AppConfig = InitialModuleConfigs;
|
export type AppConfig = InitialModuleConfigs;
|
||||||
|
|
||||||
export class App<DB = any> {
|
export class App {
|
||||||
modules: ModuleManager;
|
modules: ModuleManager;
|
||||||
static readonly Events = AppEvents;
|
static readonly Events = AppEvents;
|
||||||
|
adminController?: AdminController;
|
||||||
|
private trigger_first_boot = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private connection: Connection,
|
private connection: Connection,
|
||||||
_initialConfig?: InitialModuleConfigs,
|
_initialConfig?: InitialModuleConfigs,
|
||||||
private plugins: AppPlugin<DB>[] = [],
|
private plugins: AppPlugin[] = [],
|
||||||
moduleManagerOptions?: ModuleManagerOptions
|
moduleManagerOptions?: ModuleManagerOptions
|
||||||
) {
|
) {
|
||||||
this.modules = new ModuleManager(connection, {
|
this.modules = new ModuleManager(connection, {
|
||||||
...moduleManagerOptions,
|
...moduleManagerOptions,
|
||||||
initial: _initialConfig,
|
initial: _initialConfig,
|
||||||
onUpdated: async (key, config) => {
|
onUpdated: async (key, config) => {
|
||||||
//console.log("[APP] config updated", key, config);
|
// if the EventManager was disabled, we assume we shouldn't
|
||||||
|
// respond to events, such as "onUpdated".
|
||||||
|
if (!this.emgr.enabled) {
|
||||||
|
console.warn("[APP] config updated, but event manager is disabled, skip.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[APP] config updated", key);
|
||||||
await this.build({ sync: true, save: true });
|
await this.build({ sync: true, save: true });
|
||||||
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this }));
|
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this }));
|
||||||
|
},
|
||||||
|
onFirstBoot: async () => {
|
||||||
|
console.log("[APP] first boot");
|
||||||
|
this.trigger_first_boot = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.modules.ctx().emgr.registerEvents(AppEvents);
|
this.modules.ctx().emgr.registerEvents(AppEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
static create(config: CreateAppConfig) {
|
|
||||||
let connection: Connection | undefined = undefined;
|
|
||||||
|
|
||||||
if (Connection.isConnection(config.connection)) {
|
|
||||||
connection = config.connection;
|
|
||||||
} else if (typeof config.connection === "object") {
|
|
||||||
switch (config.connection.type) {
|
|
||||||
case "libsql":
|
|
||||||
connection = new LibsqlConnection(config.connection.config);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error(`Unknown connection of type ${typeof config.connection} given.`);
|
|
||||||
}
|
|
||||||
if (!connection) {
|
|
||||||
throw new Error("Invalid connection");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new App(connection, config.initialConfig, config.plugins, config.options);
|
|
||||||
}
|
|
||||||
|
|
||||||
get emgr() {
|
get emgr() {
|
||||||
return this.modules.ctx().emgr;
|
return this.modules.ctx().emgr;
|
||||||
}
|
}
|
||||||
@@ -97,7 +93,7 @@ export class App<DB = any> {
|
|||||||
|
|
||||||
// load plugins
|
// load plugins
|
||||||
if (this.plugins.length > 0) {
|
if (this.plugins.length > 0) {
|
||||||
this.plugins.forEach((plugin) => plugin(this));
|
await Promise.all(this.plugins.map((plugin) => plugin(this)));
|
||||||
}
|
}
|
||||||
|
|
||||||
//console.log("emitting built", options);
|
//console.log("emitting built", options);
|
||||||
@@ -109,14 +105,24 @@ export class App<DB = any> {
|
|||||||
if (options?.save) {
|
if (options?.save) {
|
||||||
await this.modules.save();
|
await this.modules.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateConfig<Module extends keyof Modules>(module: Module) {
|
mutateConfig<Module extends keyof Modules>(module: Module) {
|
||||||
return this.modules.get(module).schema();
|
return this.modules.get(module).schema();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get server() {
|
||||||
|
return this.modules.server;
|
||||||
|
}
|
||||||
|
|
||||||
get fetch(): any {
|
get fetch(): any {
|
||||||
return this.modules.server.fetch;
|
return this.server.fetch;
|
||||||
}
|
}
|
||||||
|
|
||||||
get module() {
|
get module() {
|
||||||
@@ -140,11 +146,39 @@ export class App<DB = any> {
|
|||||||
|
|
||||||
registerAdminController(config?: AdminControllerOptions) {
|
registerAdminController(config?: AdminControllerOptions) {
|
||||||
// register admin
|
// register admin
|
||||||
this.modules.server.route("/", new AdminController(this, config).getController());
|
this.adminController = new AdminController(this, config);
|
||||||
|
this.modules.server.route("/", this.adminController.getController());
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON(secrets?: boolean) {
|
toJSON(secrets?: boolean) {
|
||||||
return this.modules.toJSON(secrets);
|
return this.modules.toJSON(secrets);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static create(config: CreateAppConfig) {
|
||||||
|
return createApp(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createApp(config: CreateAppConfig = {}) {
|
||||||
|
let connection: Connection | undefined = undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (Connection.isConnection(config.connection)) {
|
||||||
|
connection = config.connection;
|
||||||
|
} else if (typeof config.connection === "object") {
|
||||||
|
connection = new LibsqlConnection(config.connection.config);
|
||||||
|
} else {
|
||||||
|
connection = new LibsqlConnection({ url: ":memory:" });
|
||||||
|
console.warn("[!] No connection provided, using in-memory database");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Could not create connection", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!connection) {
|
||||||
|
throw new Error("Invalid connection");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new App(connection, config.initialConfig, config.plugins, config.options);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { type FrameworkBkndConfig, createFrameworkApp } from "adapter";
|
||||||
|
import { Api, type ApiOptions, type App } from "bknd";
|
||||||
|
|
||||||
|
export type AstroBkndConfig = FrameworkBkndConfig;
|
||||||
|
|
||||||
|
type TAstro = {
|
||||||
|
request: Request;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Options = {
|
||||||
|
mode?: "static" | "dynamic";
|
||||||
|
} & Omit<ApiOptions, "host"> & {
|
||||||
|
host?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getApi(Astro: TAstro, options: Options = { mode: "static" }) {
|
||||||
|
return new Api({
|
||||||
|
host: new URL(Astro.request.url).origin,
|
||||||
|
headers: options.mode === "dynamic" ? Astro.request.headers : undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let app: App;
|
||||||
|
export function serve(config: AstroBkndConfig = {}) {
|
||||||
|
return async (args: TAstro) => {
|
||||||
|
if (!app) {
|
||||||
|
app = await createFrameworkApp(config);
|
||||||
|
}
|
||||||
|
return app.fetch(args.request);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./astro.adapter";
|
||||||
@@ -1,55 +1,63 @@
|
|||||||
|
/// <reference types="bun-types" />
|
||||||
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { App, type CreateAppConfig } from "bknd";
|
import type { App } from "bknd";
|
||||||
import { LibsqlConnection } from "bknd/data";
|
import type { ServeOptions } from "bun";
|
||||||
|
import { config } from "core";
|
||||||
import { serveStatic } from "hono/bun";
|
import { serveStatic } from "hono/bun";
|
||||||
|
import { type RuntimeBkndConfig, createRuntimeApp } from "../index";
|
||||||
|
|
||||||
async function getConnection(conn?: CreateAppConfig["connection"]) {
|
let app: App;
|
||||||
if (conn) {
|
|
||||||
if (LibsqlConnection.isConnection(conn)) {
|
|
||||||
return conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new LibsqlConnection(conn.config);
|
export type BunBkndConfig = RuntimeBkndConfig & Omit<ServeOptions, "fetch">;
|
||||||
}
|
|
||||||
|
|
||||||
const createClient = await import("@libsql/client/node").then((m) => m.createClient);
|
export async function createApp({
|
||||||
if (!createClient) {
|
distPath,
|
||||||
throw new Error('libsql client not found, you need to install "@libsql/client/node"');
|
onBuilt,
|
||||||
}
|
buildConfig,
|
||||||
|
beforeBuild,
|
||||||
console.log("Using in-memory database");
|
...config
|
||||||
return new LibsqlConnection(createClient({ url: ":memory:" }));
|
}: RuntimeBkndConfig = {}) {
|
||||||
}
|
|
||||||
|
|
||||||
export function serve(_config: Partial<CreateAppConfig> = {}, distPath?: string) {
|
|
||||||
const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static");
|
const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static");
|
||||||
let app: App;
|
|
||||||
|
|
||||||
return async (req: Request) => {
|
if (!app) {
|
||||||
if (!app) {
|
app = await createRuntimeApp({
|
||||||
const connection = await getConnection(_config.connection);
|
...config,
|
||||||
app = App.create({
|
registerLocalMedia: true,
|
||||||
..._config,
|
serveStatic: serveStatic({ root })
|
||||||
connection
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
app.emgr.on(
|
return app;
|
||||||
"app-built",
|
}
|
||||||
async () => {
|
|
||||||
app.modules.server.get(
|
export function serve({
|
||||||
"/*",
|
distPath,
|
||||||
serveStatic({
|
connection,
|
||||||
root
|
initialConfig,
|
||||||
})
|
plugins,
|
||||||
);
|
options,
|
||||||
app.registerAdminController();
|
port = config.server.default_port,
|
||||||
},
|
onBuilt,
|
||||||
"sync"
|
buildConfig,
|
||||||
);
|
...serveOptions
|
||||||
|
}: BunBkndConfig = {}) {
|
||||||
await app.build();
|
Bun.serve({
|
||||||
}
|
...serveOptions,
|
||||||
|
port,
|
||||||
return app.fetch(req);
|
fetch: async (request: Request) => {
|
||||||
};
|
const app = await createApp({
|
||||||
|
connection,
|
||||||
|
initialConfig,
|
||||||
|
plugins,
|
||||||
|
options,
|
||||||
|
onBuilt,
|
||||||
|
buildConfig,
|
||||||
|
distPath
|
||||||
|
});
|
||||||
|
return app.fetch(request);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Server is running on http://localhost:${port}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,37 @@
|
|||||||
import { DurableObject } from "cloudflare:workers";
|
import type { CreateAppConfig } from "bknd";
|
||||||
import { App, type CreateAppConfig } from "bknd";
|
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { serveStatic } from "hono/cloudflare-workers";
|
import { serveStatic } from "hono/cloudflare-workers";
|
||||||
import type { BkndConfig, CfBkndModeCache } from "../index";
|
import type { FrameworkBkndConfig } from "../index";
|
||||||
|
import { getCached } from "./modes/cached";
|
||||||
|
import { getDurable } from "./modes/durable";
|
||||||
|
import { getFresh, getWarm } from "./modes/fresh";
|
||||||
|
|
||||||
type Context = {
|
export type CloudflareBkndConfig<Env = any> = Omit<FrameworkBkndConfig, "app"> & {
|
||||||
request: Request;
|
app: CreateAppConfig | ((env: Env) => CreateAppConfig);
|
||||||
env: any;
|
mode?: "warm" | "fresh" | "cache" | "durable";
|
||||||
ctx: ExecutionContext;
|
bindings?: (env: Env) => {
|
||||||
manifest: any;
|
kv?: KVNamespace;
|
||||||
|
dobj?: DurableObjectNamespace;
|
||||||
|
};
|
||||||
|
key?: string;
|
||||||
|
keepAliveSeconds?: number;
|
||||||
|
forceHttps?: boolean;
|
||||||
|
manifest?: string;
|
||||||
|
setAdminHtml?: boolean;
|
||||||
html?: string;
|
html?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function serve(_config: BkndConfig, manifest?: string, html?: string) {
|
export type Context = {
|
||||||
|
request: Request;
|
||||||
|
env: any;
|
||||||
|
ctx: ExecutionContext;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function serve(config: CloudflareBkndConfig) {
|
||||||
return {
|
return {
|
||||||
async fetch(request: Request, env: any, ctx: ExecutionContext) {
|
async fetch(request: Request, env: any, ctx: ExecutionContext) {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
|
const manifest = config.manifest;
|
||||||
|
|
||||||
if (manifest) {
|
if (manifest) {
|
||||||
const pathname = url.pathname.slice(1);
|
const pathname = url.pathname.slice(1);
|
||||||
@@ -26,13 +42,10 @@ export function serve(_config: BkndConfig, manifest?: string, html?: string) {
|
|||||||
hono.all("*", async (c, next) => {
|
hono.all("*", async (c, next) => {
|
||||||
const res = await serveStatic({
|
const res = await serveStatic({
|
||||||
path: `./${pathname}`,
|
path: `./${pathname}`,
|
||||||
manifest,
|
manifest
|
||||||
onNotFound: (path) => console.log("not found", path)
|
|
||||||
})(c as any, next);
|
})(c as any, next);
|
||||||
if (res instanceof Response) {
|
if (res instanceof Response) {
|
||||||
const ttl = pathname.startsWith("assets/")
|
const ttl = 60 * 60 * 24 * 365;
|
||||||
? 60 * 60 * 24 * 365 // 1 year
|
|
||||||
: 60 * 5; // 5 minutes
|
|
||||||
res.headers.set("Cache-Control", `public, max-age=${ttl}`);
|
res.headers.set("Cache-Control", `public, max-age=${ttl}`);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
@@ -44,214 +57,23 @@ export function serve(_config: BkndConfig, manifest?: string, html?: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = {
|
config.setAdminHtml = config.setAdminHtml && !!config.manifest;
|
||||||
..._config,
|
|
||||||
setAdminHtml: _config.setAdminHtml ?? !!manifest
|
|
||||||
};
|
|
||||||
const context = { request, env, ctx, manifest, html };
|
|
||||||
const mode = config.cloudflare?.mode?.(env);
|
|
||||||
|
|
||||||
if (!mode) {
|
const context = { request, env, ctx } as Context;
|
||||||
console.log("serving fresh...");
|
const mode = config.mode ?? "warm";
|
||||||
const app = await getFresh(config, context);
|
|
||||||
return app.fetch(request, env);
|
|
||||||
} else if ("cache" in mode) {
|
|
||||||
console.log("serving cached...");
|
|
||||||
const app = await getCached(config as any, context);
|
|
||||||
return app.fetch(request, env);
|
|
||||||
} else if ("durableObject" in mode) {
|
|
||||||
console.log("serving durable...");
|
|
||||||
|
|
||||||
if (config.onBuilt) {
|
switch (mode) {
|
||||||
console.log("onBuilt() is not supported with DurableObject mode");
|
case "fresh":
|
||||||
}
|
return await getFresh(config, context);
|
||||||
|
case "warm":
|
||||||
const start = performance.now();
|
return await getWarm(config, context);
|
||||||
|
case "cache":
|
||||||
const durable = mode.durableObject;
|
return await getCached(config, context);
|
||||||
const id = durable.idFromName(mode.key);
|
case "durable":
|
||||||
const stub = durable.get(id) as unknown as DurableBkndApp;
|
return await getDurable(config, context);
|
||||||
|
default:
|
||||||
const create_config = typeof config.app === "function" ? config.app(env) : config.app;
|
throw new Error(`Unknown mode ${mode}`);
|
||||||
|
|
||||||
const res = await stub.fire(request, {
|
|
||||||
config: create_config,
|
|
||||||
html,
|
|
||||||
keepAliveSeconds: mode.keepAliveSeconds,
|
|
||||||
setAdminHtml: config.setAdminHtml
|
|
||||||
});
|
|
||||||
|
|
||||||
const headers = new Headers(res.headers);
|
|
||||||
headers.set("X-TTDO", String(performance.now() - start));
|
|
||||||
|
|
||||||
return new Response(res.body, {
|
|
||||||
status: res.status,
|
|
||||||
statusText: res.statusText,
|
|
||||||
headers
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getFresh(config: BkndConfig, { env, html }: Context) {
|
|
||||||
const create_config = typeof config.app === "function" ? config.app(env) : config.app;
|
|
||||||
const app = App.create(create_config);
|
|
||||||
|
|
||||||
if (config.onBuilt) {
|
|
||||||
app.emgr.onEvent(
|
|
||||||
App.Events.AppBuiltEvent,
|
|
||||||
async ({ params: { app } }) => {
|
|
||||||
config.onBuilt!(app);
|
|
||||||
},
|
|
||||||
"sync"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await app.build();
|
|
||||||
|
|
||||||
if (config.setAdminHtml) {
|
|
||||||
app.registerAdminController({ html });
|
|
||||||
}
|
|
||||||
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getCached(
|
|
||||||
config: BkndConfig & { cloudflare: { mode: CfBkndModeCache } },
|
|
||||||
{ env, html, ctx }: Context
|
|
||||||
) {
|
|
||||||
const { cache, key } = config.cloudflare.mode(env) as ReturnType<CfBkndModeCache>;
|
|
||||||
const create_config = typeof config.app === "function" ? config.app(env) : config.app;
|
|
||||||
|
|
||||||
const cachedConfig = await cache.get(key);
|
|
||||||
const initialConfig = cachedConfig ? JSON.parse(cachedConfig) : undefined;
|
|
||||||
|
|
||||||
const app = App.create({ ...create_config, initialConfig });
|
|
||||||
|
|
||||||
async function saveConfig(__config: any) {
|
|
||||||
ctx.waitUntil(cache.put(key, JSON.stringify(__config)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.onBuilt) {
|
|
||||||
app.emgr.onEvent(
|
|
||||||
App.Events.AppBuiltEvent,
|
|
||||||
async ({ params: { app } }) => {
|
|
||||||
app.module.server.client.get("/__bknd/cache", async (c) => {
|
|
||||||
await cache.delete(key);
|
|
||||||
return c.json({ message: "Cache cleared" });
|
|
||||||
});
|
|
||||||
app.registerAdminController({ html });
|
|
||||||
|
|
||||||
config.onBuilt!(app);
|
|
||||||
},
|
|
||||||
"sync"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
app.emgr.onEvent(
|
|
||||||
App.Events.AppConfigUpdatedEvent,
|
|
||||||
async ({ params: { app } }) => {
|
|
||||||
saveConfig(app.toJSON(true));
|
|
||||||
},
|
|
||||||
"sync"
|
|
||||||
);
|
|
||||||
|
|
||||||
await app.build();
|
|
||||||
|
|
||||||
if (config.setAdminHtml) {
|
|
||||||
app.registerAdminController({ html });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cachedConfig) {
|
|
||||||
saveConfig(app.toJSON(true));
|
|
||||||
}
|
|
||||||
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DurableBkndApp extends DurableObject {
|
|
||||||
protected id = Math.random().toString(36).slice(2);
|
|
||||||
protected app?: App;
|
|
||||||
protected interval?: any;
|
|
||||||
|
|
||||||
async fire(
|
|
||||||
request: Request,
|
|
||||||
options: {
|
|
||||||
config: CreateAppConfig;
|
|
||||||
html?: string;
|
|
||||||
keepAliveSeconds?: number;
|
|
||||||
setAdminHtml?: boolean;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
let buildtime = 0;
|
|
||||||
if (!this.app) {
|
|
||||||
const start = performance.now();
|
|
||||||
const config = options.config;
|
|
||||||
|
|
||||||
// change protocol to websocket if libsql
|
|
||||||
if ("type" in config.connection && config.connection.type === "libsql") {
|
|
||||||
config.connection.config.protocol = "wss";
|
|
||||||
}
|
|
||||||
|
|
||||||
this.app = App.create(config);
|
|
||||||
this.app.emgr.onEvent(
|
|
||||||
App.Events.AppBuiltEvent,
|
|
||||||
async ({ params: { app } }) => {
|
|
||||||
app.modules.server.get("/__do", async (c) => {
|
|
||||||
// @ts-ignore
|
|
||||||
const context: any = c.req.raw.cf ? c.req.raw.cf : c.env.cf;
|
|
||||||
return c.json({
|
|
||||||
id: this.id,
|
|
||||||
keepAlive: options?.keepAliveSeconds,
|
|
||||||
colo: context.colo
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
"sync"
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.app.build();
|
|
||||||
|
|
||||||
buildtime = performance.now() - start;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options?.keepAliveSeconds) {
|
|
||||||
this.keepAlive(options.keepAliveSeconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("id", this.id);
|
|
||||||
const res = await this.app!.fetch(request);
|
|
||||||
const headers = new Headers(res.headers);
|
|
||||||
headers.set("X-BuildTime", buildtime.toString());
|
|
||||||
headers.set("X-DO-ID", this.id);
|
|
||||||
|
|
||||||
return new Response(res.body, {
|
|
||||||
status: res.status,
|
|
||||||
statusText: res.statusText,
|
|
||||||
headers
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected keepAlive(seconds: number) {
|
|
||||||
console.log("keep alive for", seconds);
|
|
||||||
if (this.interval) {
|
|
||||||
console.log("clearing, there is a new");
|
|
||||||
clearInterval(this.interval);
|
|
||||||
}
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
this.interval = setInterval(() => {
|
|
||||||
i += 1;
|
|
||||||
//console.log("keep-alive", i);
|
|
||||||
if (i === seconds) {
|
|
||||||
console.log("cleared");
|
|
||||||
clearInterval(this.interval);
|
|
||||||
|
|
||||||
// ping every 30 seconds
|
|
||||||
} else if (i % 30 === 0) {
|
|
||||||
console.log("ping");
|
|
||||||
this.app?.modules.ctx().connection.ping();
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1 +1,4 @@
|
|||||||
export * from "./cloudflare-workers.adapter";
|
export * from "./cloudflare-workers.adapter";
|
||||||
|
export { makeApp, getFresh, getWarm } from "./modes/fresh";
|
||||||
|
export { getCached } from "./modes/cached";
|
||||||
|
export { DurableBkndApp, getDurable } from "./modes/durable";
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { createRuntimeApp } from "adapter";
|
||||||
|
import { App } from "bknd";
|
||||||
|
import type { CloudflareBkndConfig, Context } from "../index";
|
||||||
|
|
||||||
|
export async function getCached(config: CloudflareBkndConfig, { env, ctx }: Context) {
|
||||||
|
const { kv } = config.bindings?.(env)!;
|
||||||
|
if (!kv) throw new Error("kv namespace is not defined in cloudflare.bindings");
|
||||||
|
const key = config.key ?? "app";
|
||||||
|
|
||||||
|
const cachedConfig = await kv.get(key);
|
||||||
|
const initialConfig = cachedConfig ? JSON.parse(cachedConfig) : undefined;
|
||||||
|
|
||||||
|
async function saveConfig(__config: any) {
|
||||||
|
ctx.waitUntil(kv!.put(key, JSON.stringify(__config)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = await createRuntimeApp(
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
initialConfig,
|
||||||
|
onBuilt: async (app) => {
|
||||||
|
app.module.server.client.get("/__bknd/cache", async (c) => {
|
||||||
|
await kv.delete(key);
|
||||||
|
return c.json({ message: "Cache cleared" });
|
||||||
|
});
|
||||||
|
await config.onBuilt?.(app);
|
||||||
|
},
|
||||||
|
beforeBuild: async (app) => {
|
||||||
|
app.emgr.onEvent(
|
||||||
|
App.Events.AppConfigUpdatedEvent,
|
||||||
|
async ({ params: { app } }) => {
|
||||||
|
saveConfig(app.toJSON(true));
|
||||||
|
},
|
||||||
|
"sync"
|
||||||
|
);
|
||||||
|
await config.beforeBuild?.(app);
|
||||||
|
},
|
||||||
|
adminOptions: { html: config.html }
|
||||||
|
},
|
||||||
|
env
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!cachedConfig) {
|
||||||
|
saveConfig(app.toJSON(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { DurableObject } from "cloudflare:workers";
|
||||||
|
import { createRuntimeApp } from "adapter";
|
||||||
|
import type { CloudflareBkndConfig, Context } from "adapter/cloudflare";
|
||||||
|
import type { App, CreateAppConfig } from "bknd";
|
||||||
|
|
||||||
|
export async function getDurable(config: CloudflareBkndConfig, ctx: Context) {
|
||||||
|
const { dobj } = config.bindings?.(ctx.env)!;
|
||||||
|
if (!dobj) throw new Error("durable object is not defined in cloudflare.bindings");
|
||||||
|
const key = config.key ?? "app";
|
||||||
|
|
||||||
|
if ([config.onBuilt, config.beforeBuild].some((x) => x)) {
|
||||||
|
console.log("onBuilt and beforeBuild are not supported with DurableObject mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = performance.now();
|
||||||
|
|
||||||
|
const id = dobj.idFromName(key);
|
||||||
|
const stub = dobj.get(id) as unknown as DurableBkndApp;
|
||||||
|
|
||||||
|
const create_config = typeof config.app === "function" ? config.app(ctx.env) : config.app;
|
||||||
|
|
||||||
|
const res = await stub.fire(ctx.request, {
|
||||||
|
config: create_config,
|
||||||
|
html: config.html,
|
||||||
|
keepAliveSeconds: config.keepAliveSeconds,
|
||||||
|
setAdminHtml: config.setAdminHtml
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers = new Headers(res.headers);
|
||||||
|
headers.set("X-TTDO", String(performance.now() - start));
|
||||||
|
|
||||||
|
return new Response(res.body, {
|
||||||
|
status: res.status,
|
||||||
|
statusText: res.statusText,
|
||||||
|
headers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DurableBkndApp extends DurableObject {
|
||||||
|
protected id = Math.random().toString(36).slice(2);
|
||||||
|
protected app?: App;
|
||||||
|
protected interval?: any;
|
||||||
|
|
||||||
|
async fire(
|
||||||
|
request: Request,
|
||||||
|
options: {
|
||||||
|
config: CreateAppConfig;
|
||||||
|
html?: string;
|
||||||
|
keepAliveSeconds?: number;
|
||||||
|
setAdminHtml?: boolean;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
let buildtime = 0;
|
||||||
|
if (!this.app) {
|
||||||
|
const start = performance.now();
|
||||||
|
const config = options.config;
|
||||||
|
|
||||||
|
// change protocol to websocket if libsql
|
||||||
|
if (
|
||||||
|
config?.connection &&
|
||||||
|
"type" in config.connection &&
|
||||||
|
config.connection.type === "libsql"
|
||||||
|
) {
|
||||||
|
config.connection.config.protocol = "wss";
|
||||||
|
}
|
||||||
|
|
||||||
|
this.app = await createRuntimeApp({
|
||||||
|
...config,
|
||||||
|
onBuilt: async (app) => {
|
||||||
|
app.modules.server.get("/__do", async (c) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const context: any = c.req.raw.cf ? c.req.raw.cf : c.env.cf;
|
||||||
|
return c.json({
|
||||||
|
id: this.id,
|
||||||
|
keepAliveSeconds: options?.keepAliveSeconds ?? 0,
|
||||||
|
colo: context.colo
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.onBuilt(app);
|
||||||
|
},
|
||||||
|
adminOptions: { html: options.html },
|
||||||
|
beforeBuild: async (app) => {
|
||||||
|
await this.beforeBuild(app);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
buildtime = performance.now() - start;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.keepAliveSeconds) {
|
||||||
|
this.keepAlive(options.keepAliveSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("id", this.id);
|
||||||
|
const res = await this.app!.fetch(request);
|
||||||
|
const headers = new Headers(res.headers);
|
||||||
|
headers.set("X-BuildTime", buildtime.toString());
|
||||||
|
headers.set("X-DO-ID", this.id);
|
||||||
|
|
||||||
|
return new Response(res.body, {
|
||||||
|
status: res.status,
|
||||||
|
statusText: res.statusText,
|
||||||
|
headers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onBuilt(app: App) {}
|
||||||
|
async beforeBuild(app: App) {}
|
||||||
|
|
||||||
|
protected keepAlive(seconds: number) {
|
||||||
|
console.log("keep alive for", seconds);
|
||||||
|
if (this.interval) {
|
||||||
|
console.log("clearing, there is a new");
|
||||||
|
clearInterval(this.interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
this.interval = setInterval(() => {
|
||||||
|
i += 1;
|
||||||
|
//console.log("keep-alive", i);
|
||||||
|
if (i === seconds) {
|
||||||
|
console.log("cleared");
|
||||||
|
clearInterval(this.interval);
|
||||||
|
|
||||||
|
// ping every 30 seconds
|
||||||
|
} else if (i % 30 === 0) {
|
||||||
|
console.log("ping");
|
||||||
|
this.app?.modules.ctx().connection.ping();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { createRuntimeApp } from "adapter";
|
||||||
|
import type { App } from "bknd";
|
||||||
|
import type { CloudflareBkndConfig, Context } from "../index";
|
||||||
|
|
||||||
|
export async function makeApp(config: CloudflareBkndConfig, { env }: Context) {
|
||||||
|
return await createRuntimeApp(
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
adminOptions: config.html ? { html: config.html } : undefined
|
||||||
|
},
|
||||||
|
env
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFresh(config: CloudflareBkndConfig, ctx: Context) {
|
||||||
|
const app = await makeApp(config, ctx);
|
||||||
|
return app.fetch(ctx.request);
|
||||||
|
}
|
||||||
|
|
||||||
|
let warm_app: App;
|
||||||
|
export async function getWarm(config: CloudflareBkndConfig, ctx: Context) {
|
||||||
|
if (!warm_app) {
|
||||||
|
warm_app = await makeApp(config, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
return warm_app.fetch(ctx.request);
|
||||||
|
}
|
||||||
+99
-32
@@ -1,40 +1,20 @@
|
|||||||
import type { IncomingMessage } from "node:http";
|
import type { IncomingMessage } from "node:http";
|
||||||
import type { App, CreateAppConfig } from "bknd";
|
import { App, type CreateAppConfig, registries } from "bknd";
|
||||||
|
import type { MiddlewareHandler } from "hono";
|
||||||
|
import { StorageLocalAdapter } from "media/storage/adapters/StorageLocalAdapter";
|
||||||
|
import type { AdminControllerOptions } from "modules/server/AdminController";
|
||||||
|
|
||||||
export type CfBkndModeCache<Env = any> = (env: Env) => {
|
export type BkndConfig<Env = any> = CreateAppConfig & {
|
||||||
cache: KVNamespace;
|
app?: CreateAppConfig | ((env: Env) => CreateAppConfig);
|
||||||
key: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CfBkndModeDurableObject<Env = any> = (env: Env) => {
|
|
||||||
durableObject: DurableObjectNamespace;
|
|
||||||
key: string;
|
|
||||||
keepAliveSeconds?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CloudflareBkndConfig<Env = any> = {
|
|
||||||
mode?: CfBkndModeCache | CfBkndModeDurableObject;
|
|
||||||
forceHttps?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
// @todo: move to App
|
|
||||||
export type BkndConfig<Env = any> = {
|
|
||||||
app: CreateAppConfig | ((env: Env) => CreateAppConfig);
|
|
||||||
setAdminHtml?: boolean;
|
|
||||||
server?: {
|
|
||||||
port?: number;
|
|
||||||
platform?: "node" | "bun";
|
|
||||||
};
|
|
||||||
cloudflare?: CloudflareBkndConfig<Env>;
|
|
||||||
onBuilt?: (app: App) => Promise<void>;
|
onBuilt?: (app: App) => Promise<void>;
|
||||||
|
beforeBuild?: (app: App) => Promise<void>;
|
||||||
|
buildConfig?: Parameters<App["build"]>[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BkndConfigJson = {
|
export type FrameworkBkndConfig<Env = any> = BkndConfig<Env>;
|
||||||
app: CreateAppConfig;
|
|
||||||
setAdminHtml?: boolean;
|
export type RuntimeBkndConfig<Env = any> = BkndConfig<Env> & {
|
||||||
server?: {
|
distPath?: string;
|
||||||
port?: number;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function nodeRequestToRequest(req: IncomingMessage): Request {
|
export function nodeRequestToRequest(req: IncomingMessage): Request {
|
||||||
@@ -60,3 +40,90 @@ export function nodeRequestToRequest(req: IncomingMessage): Request {
|
|||||||
headers
|
headers
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function registerLocalMediaAdapter() {
|
||||||
|
registries.media.register("local", StorageLocalAdapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeConfig<Env = any>(config: BkndConfig<Env>, env?: Env): CreateAppConfig {
|
||||||
|
let additionalConfig: CreateAppConfig = {};
|
||||||
|
if ("app" in config && config.app) {
|
||||||
|
if (typeof config.app === "function") {
|
||||||
|
if (!env) {
|
||||||
|
throw new Error("env is required when config.app is a function");
|
||||||
|
}
|
||||||
|
additionalConfig = config.app(env);
|
||||||
|
} else {
|
||||||
|
additionalConfig = config.app;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...config, ...additionalConfig };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createFrameworkApp<Env = any>(
|
||||||
|
config: FrameworkBkndConfig,
|
||||||
|
env?: Env
|
||||||
|
): Promise<App> {
|
||||||
|
const app = App.create(makeConfig(config, env));
|
||||||
|
|
||||||
|
if (config.onBuilt) {
|
||||||
|
app.emgr.onEvent(
|
||||||
|
App.Events.AppBuiltEvent,
|
||||||
|
async () => {
|
||||||
|
await config.onBuilt?.(app);
|
||||||
|
},
|
||||||
|
"sync"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await config.beforeBuild?.(app);
|
||||||
|
await app.build(config.buildConfig);
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createRuntimeApp<Env = any>(
|
||||||
|
{
|
||||||
|
serveStatic,
|
||||||
|
registerLocalMedia,
|
||||||
|
adminOptions,
|
||||||
|
...config
|
||||||
|
}: RuntimeBkndConfig & {
|
||||||
|
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
|
||||||
|
registerLocalMedia?: boolean;
|
||||||
|
adminOptions?: AdminControllerOptions | false;
|
||||||
|
},
|
||||||
|
env?: Env
|
||||||
|
): Promise<App> {
|
||||||
|
if (registerLocalMedia) {
|
||||||
|
registerLocalMediaAdapter();
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = App.create(makeConfig(config, env));
|
||||||
|
|
||||||
|
app.emgr.onEvent(
|
||||||
|
App.Events.AppBuiltEvent,
|
||||||
|
async () => {
|
||||||
|
if (serveStatic) {
|
||||||
|
if (Array.isArray(serveStatic)) {
|
||||||
|
const [path, handler] = serveStatic;
|
||||||
|
app.modules.server.get(path, handler);
|
||||||
|
} else {
|
||||||
|
app.modules.server.get("/*", serveStatic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await config.onBuilt?.(app);
|
||||||
|
if (adminOptions !== false) {
|
||||||
|
app.registerAdminController(adminOptions);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sync"
|
||||||
|
);
|
||||||
|
|
||||||
|
await config.beforeBuild?.(app);
|
||||||
|
await app.build(config.buildConfig);
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import { withApi } from "bknd/adapter/nextjs";
|
|
||||||
import type { InferGetServerSidePropsType } from "next";
|
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
|
|
||||||
export const getServerSideProps = withApi(async (context) => {
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
user: context.api.getUser()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export function adminPage() {
|
|
||||||
const Admin = dynamic(() => import("bknd/ui").then((mod) => mod.Admin), { ssr: false });
|
|
||||||
const ClientProvider = dynamic(() => import("bknd/ui").then((mod) => mod.ClientProvider));
|
|
||||||
return (props: InferGetServerSidePropsType<typeof getServerSideProps>) => {
|
|
||||||
if (typeof document === "undefined") return null;
|
|
||||||
return (
|
|
||||||
<ClientProvider user={props.user}>
|
|
||||||
<Admin />
|
|
||||||
</ClientProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,2 +1 @@
|
|||||||
export * from "./nextjs.adapter";
|
export * from "./nextjs.adapter";
|
||||||
export * from "./AdminPage";
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||||
import { Api, App, type CreateAppConfig } from "bknd";
|
import { Api, type App } from "bknd";
|
||||||
import { nodeRequestToRequest } from "../index";
|
import { type FrameworkBkndConfig, createFrameworkApp, nodeRequestToRequest } from "../index";
|
||||||
|
|
||||||
|
export type NextjsBkndConfig = FrameworkBkndConfig;
|
||||||
|
|
||||||
type GetServerSidePropsContext = {
|
type GetServerSidePropsContext = {
|
||||||
req: IncomingMessage;
|
req: IncomingMessage;
|
||||||
@@ -18,7 +20,6 @@ type GetServerSidePropsContext = {
|
|||||||
|
|
||||||
export function createApi({ req }: GetServerSidePropsContext) {
|
export function createApi({ req }: GetServerSidePropsContext) {
|
||||||
const request = nodeRequestToRequest(req);
|
const request = nodeRequestToRequest(req);
|
||||||
//console.log("createApi:request.headers", request.headers);
|
|
||||||
return new Api({
|
return new Api({
|
||||||
host: new URL(request.url).origin,
|
host: new URL(request.url).origin,
|
||||||
headers: request.headers
|
headers: request.headers
|
||||||
@@ -43,11 +44,10 @@ function getCleanRequest(req: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let app: App;
|
let app: App;
|
||||||
export function serve(config: CreateAppConfig) {
|
export function serve(config: NextjsBkndConfig = {}) {
|
||||||
return async (req: Request) => {
|
return async (req: Request) => {
|
||||||
if (!app) {
|
if (!app) {
|
||||||
app = App.create(config);
|
app = await createFrameworkApp(config);
|
||||||
await app.build();
|
|
||||||
}
|
}
|
||||||
const request = getCleanRequest(req);
|
const request = getCleanRequest(req);
|
||||||
return app.fetch(request, process.env);
|
return app.fetch(request, process.env);
|
||||||
|
|||||||
@@ -1,73 +1,6 @@
|
|||||||
import path from "node:path";
|
export * from "./node.adapter";
|
||||||
import { serve as honoServe } from "@hono/node-server";
|
export {
|
||||||
import { serveStatic } from "@hono/node-server/serve-static";
|
StorageLocalAdapter,
|
||||||
import { App, type CreateAppConfig } from "bknd";
|
type LocalAdapterConfig
|
||||||
import { LibsqlConnection } from "bknd/data";
|
} from "../../media/storage/adapters/StorageLocalAdapter";
|
||||||
|
export { registerLocalMediaAdapter } from "../index";
|
||||||
async function getConnection(conn?: CreateAppConfig["connection"]) {
|
|
||||||
if (conn) {
|
|
||||||
if (LibsqlConnection.isConnection(conn)) {
|
|
||||||
return conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new LibsqlConnection(conn.config);
|
|
||||||
}
|
|
||||||
|
|
||||||
const createClient = await import("@libsql/client/node").then((m) => m.createClient);
|
|
||||||
if (!createClient) {
|
|
||||||
throw new Error('libsql client not found, you need to install "@libsql/client/node"');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Using in-memory database");
|
|
||||||
return new LibsqlConnection(createClient({ url: ":memory:" }));
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NodeAdapterOptions = {
|
|
||||||
relativeDistPath?: string;
|
|
||||||
port?: number;
|
|
||||||
hostname?: string;
|
|
||||||
listener?: Parameters<typeof honoServe>[1];
|
|
||||||
};
|
|
||||||
|
|
||||||
export function serve(_config: Partial<CreateAppConfig> = {}, options: NodeAdapterOptions = {}) {
|
|
||||||
const root = path.relative(
|
|
||||||
process.cwd(),
|
|
||||||
path.resolve(options.relativeDistPath ?? "./node_modules/bknd/dist", "static")
|
|
||||||
);
|
|
||||||
let app: App;
|
|
||||||
|
|
||||||
honoServe(
|
|
||||||
{
|
|
||||||
port: options.port ?? 1337,
|
|
||||||
hostname: options.hostname,
|
|
||||||
fetch: async (req: Request) => {
|
|
||||||
if (!app) {
|
|
||||||
const connection = await getConnection(_config.connection);
|
|
||||||
app = App.create({
|
|
||||||
..._config,
|
|
||||||
connection
|
|
||||||
});
|
|
||||||
|
|
||||||
app.emgr.on(
|
|
||||||
"app-built",
|
|
||||||
async () => {
|
|
||||||
app.modules.server.get(
|
|
||||||
"/*",
|
|
||||||
serveStatic({
|
|
||||||
root
|
|
||||||
})
|
|
||||||
);
|
|
||||||
app.registerAdminController();
|
|
||||||
},
|
|
||||||
"sync"
|
|
||||||
);
|
|
||||||
|
|
||||||
await app.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
return app.fetch(req);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
options.listener
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { serve as honoServe } from "@hono/node-server";
|
||||||
|
import { serveStatic } from "@hono/node-server/serve-static";
|
||||||
|
import type { App } from "bknd";
|
||||||
|
import { config as $config } from "core";
|
||||||
|
import { type RuntimeBkndConfig, createRuntimeApp } from "../index";
|
||||||
|
|
||||||
|
export type NodeBkndConfig = RuntimeBkndConfig & {
|
||||||
|
port?: number;
|
||||||
|
hostname?: string;
|
||||||
|
listener?: Parameters<typeof honoServe>[1];
|
||||||
|
/** @deprecated */
|
||||||
|
relativeDistPath?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function serve({
|
||||||
|
distPath,
|
||||||
|
relativeDistPath,
|
||||||
|
port = $config.server.default_port,
|
||||||
|
hostname,
|
||||||
|
listener,
|
||||||
|
onBuilt,
|
||||||
|
buildConfig = {},
|
||||||
|
beforeBuild,
|
||||||
|
...config
|
||||||
|
}: NodeBkndConfig = {}) {
|
||||||
|
const root = path.relative(
|
||||||
|
process.cwd(),
|
||||||
|
path.resolve(distPath ?? relativeDistPath ?? "./node_modules/bknd/dist", "static")
|
||||||
|
);
|
||||||
|
if (relativeDistPath) {
|
||||||
|
console.warn("relativeDistPath is deprecated, please use distPath instead");
|
||||||
|
}
|
||||||
|
|
||||||
|
let app: App;
|
||||||
|
|
||||||
|
honoServe(
|
||||||
|
{
|
||||||
|
port,
|
||||||
|
hostname,
|
||||||
|
fetch: async (req: Request) => {
|
||||||
|
if (!app) {
|
||||||
|
app = await createRuntimeApp({
|
||||||
|
...config,
|
||||||
|
registerLocalMedia: true,
|
||||||
|
serveStatic: serveStatic({ root })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.fetch(req);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(connInfo) => {
|
||||||
|
console.log(`Server is running on http://localhost:${connInfo.port}`);
|
||||||
|
listener?.(connInfo);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import type { BkndAdminProps } from "bknd/ui";
|
||||||
import { Suspense, lazy, useEffect, useState } from "react";
|
import { Suspense, lazy, useEffect, useState } from "react";
|
||||||
|
|
||||||
export function adminPage() {
|
export function adminPage(props?: BkndAdminProps) {
|
||||||
const Admin = lazy(() => import("bknd/ui").then((mod) => ({ default: mod.Admin })));
|
const Admin = lazy(() => import("bknd/ui").then((mod) => ({ default: mod.Admin })));
|
||||||
return () => {
|
return () => {
|
||||||
const [loaded, setLoaded] = useState(false);
|
const [loaded, setLoaded] = useState(false);
|
||||||
@@ -12,7 +13,7 @@ export function adminPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<Admin />
|
<Admin {...props} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { App, type CreateAppConfig } from "bknd";
|
import { type FrameworkBkndConfig, createFrameworkApp } from "adapter";
|
||||||
|
import type { App } from "bknd";
|
||||||
|
|
||||||
|
export type RemixBkndConfig = FrameworkBkndConfig;
|
||||||
|
|
||||||
let app: App;
|
let app: App;
|
||||||
export function serve(config: CreateAppConfig) {
|
export function serve(config: RemixBkndConfig = {}) {
|
||||||
return async (args: { request: Request }) => {
|
return async (args: { request: Request }) => {
|
||||||
if (!app) {
|
if (!app) {
|
||||||
app = App.create(config);
|
app = await createFrameworkApp(config);
|
||||||
await app.build();
|
|
||||||
}
|
}
|
||||||
return app.fetch(args.request);
|
return app.fetch(args.request);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,47 +1,57 @@
|
|||||||
import { serveStatic } from "@hono/node-server/serve-static";
|
import { serveStatic } from "@hono/node-server/serve-static";
|
||||||
import type { BkndConfig } from "bknd";
|
import { type RuntimeBkndConfig, createRuntimeApp } from "adapter";
|
||||||
import { App } from "bknd";
|
import type { App } from "bknd";
|
||||||
|
|
||||||
function createApp(config: BkndConfig, env: any) {
|
export type ViteBkndConfig<Env = any> = RuntimeBkndConfig<Env> & {
|
||||||
const create_config = typeof config.app === "function" ? config.app(env) : config.app;
|
setAdminHtml?: boolean;
|
||||||
return App.create(create_config);
|
forceDev?: boolean;
|
||||||
}
|
html?: string;
|
||||||
|
};
|
||||||
|
|
||||||
function setAppBuildListener(app: App, config: BkndConfig, html?: string) {
|
export function addViteScript(html: string, addBkndContext: boolean = true) {
|
||||||
app.emgr.on(
|
return html.replace(
|
||||||
"app-built",
|
"</head>",
|
||||||
async () => {
|
`<script type="module">
|
||||||
await config.onBuilt?.(app);
|
import RefreshRuntime from "/@react-refresh"
|
||||||
if (config.setAdminHtml) {
|
RefreshRuntime.injectIntoGlobalHook(window)
|
||||||
app.registerAdminController({ html, forceDev: true });
|
window.$RefreshReg$ = () => {}
|
||||||
app.module.server.client.get("/assets/*", serveStatic({ root: "./" }));
|
window.$RefreshSig$ = () => (type) => type
|
||||||
}
|
window.__vite_plugin_react_preamble_installed__ = true
|
||||||
},
|
</script>
|
||||||
"sync"
|
<script type="module" src="/@vite/client"></script>
|
||||||
|
${addBkndContext ? "<!-- BKND_CONTEXT -->" : ""}
|
||||||
|
</head>`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function serveFresh(config: BkndConfig, _html?: string) {
|
async function createApp(config: ViteBkndConfig, env?: any) {
|
||||||
|
return await createRuntimeApp(
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
adminOptions: config.setAdminHtml
|
||||||
|
? { html: config.html, forceDev: config.forceDev }
|
||||||
|
: undefined,
|
||||||
|
serveStatic: ["/assets/*", serveStatic({ root: config.distPath ?? "./" })]
|
||||||
|
},
|
||||||
|
env
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function serveFresh(config: ViteBkndConfig) {
|
||||||
return {
|
return {
|
||||||
async fetch(request: Request, env: any, ctx: ExecutionContext) {
|
async fetch(request: Request, env: any, ctx: ExecutionContext) {
|
||||||
const app = createApp(config, env);
|
const app = await createApp(config, env);
|
||||||
|
|
||||||
setAppBuildListener(app, config, _html);
|
|
||||||
await app.build();
|
|
||||||
|
|
||||||
return app.fetch(request, env, ctx);
|
return app.fetch(request, env, ctx);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let app: App;
|
let app: App;
|
||||||
export async function serveCached(config: BkndConfig, _html?: string) {
|
export async function serveCached(config: ViteBkndConfig) {
|
||||||
return {
|
return {
|
||||||
async fetch(request: Request, env: any, ctx: ExecutionContext) {
|
async fetch(request: Request, env: any, ctx: ExecutionContext) {
|
||||||
if (!app) {
|
if (!app) {
|
||||||
app = createApp(config, env);
|
app = await createApp(config, env);
|
||||||
setAppBuildListener(app, config, _html);
|
|
||||||
await app.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return app.fetch(request, env, ctx);
|
return app.fetch(request, env, ctx);
|
||||||
|
|||||||
+29
-6
@@ -1,5 +1,6 @@
|
|||||||
import { type AuthAction, Authenticator, type ProfileExchange, Role, type Strategy } from "auth";
|
import { type AuthAction, Authenticator, type ProfileExchange, Role, type Strategy } from "auth";
|
||||||
import { Exception } from "core";
|
import type { PasswordStrategy } from "auth/authenticate/strategies";
|
||||||
|
import { Exception, type PrimaryFieldType } from "core";
|
||||||
import { type Static, secureRandomString, transformObject } from "core/utils";
|
import { type Static, secureRandomString, transformObject } from "core/utils";
|
||||||
import { type Entity, EntityIndex, type EntityManager } from "data";
|
import { type Entity, EntityIndex, type EntityManager } from "data";
|
||||||
import { type FieldSchema, entity, enumm, make, text } from "data/prototype";
|
import { type FieldSchema, entity, enumm, make, text } from "data/prototype";
|
||||||
@@ -9,9 +10,9 @@ import { AuthController } from "./api/AuthController";
|
|||||||
import { type AppAuthSchema, STRATEGIES, authConfigSchema } from "./auth-schema";
|
import { type AppAuthSchema, STRATEGIES, authConfigSchema } from "./auth-schema";
|
||||||
|
|
||||||
export type UserFieldSchema = FieldSchema<typeof AppAuth.usersFields>;
|
export type UserFieldSchema = FieldSchema<typeof AppAuth.usersFields>;
|
||||||
declare global {
|
declare module "core" {
|
||||||
interface DB {
|
interface DB {
|
||||||
users: UserFieldSchema;
|
users: { id: PrimaryFieldType } & UserFieldSchema;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +101,7 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
|||||||
return this._authenticator!;
|
return this._authenticator!;
|
||||||
}
|
}
|
||||||
|
|
||||||
get em(): EntityManager<DB> {
|
get em(): EntityManager {
|
||||||
return this.ctx.em as any;
|
return this.ctx.em as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +161,9 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
|||||||
|
|
||||||
const users = this.getUsersEntity();
|
const users = this.getUsersEntity();
|
||||||
this.toggleStrategyValueVisibility(true);
|
this.toggleStrategyValueVisibility(true);
|
||||||
const result = await this.em.repo(users).findOne({ email: profile.email! });
|
const result = await this.em
|
||||||
|
.repo(users as unknown as "users")
|
||||||
|
.findOne({ email: profile.email! });
|
||||||
this.toggleStrategyValueVisibility(false);
|
this.toggleStrategyValueVisibility(false);
|
||||||
if (!result.data) {
|
if (!result.data) {
|
||||||
throw new Exception("User not found", 404);
|
throw new Exception("User not found", 404);
|
||||||
@@ -197,7 +200,7 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
|||||||
throw new Exception("User already exists");
|
throw new Exception("User already exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload: any = {
|
||||||
...profile,
|
...profile,
|
||||||
strategy: strategy.getName(),
|
strategy: strategy.getName(),
|
||||||
strategy_value: identifier
|
strategy_value: identifier
|
||||||
@@ -284,6 +287,26 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createUser({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
...additional
|
||||||
|
}: { email: string; password: string; [key: string]: any }) {
|
||||||
|
const strategy = "password";
|
||||||
|
const pw = this.authenticator.strategy(strategy) as PasswordStrategy;
|
||||||
|
const strategy_value = await pw.hash(password);
|
||||||
|
const mutator = this.em.mutator(this.config.entity_name as "users");
|
||||||
|
mutator.__unstable_toggleSystemEntityCreation(false);
|
||||||
|
const { data: created } = await mutator.insertOne({
|
||||||
|
...(additional as any),
|
||||||
|
email,
|
||||||
|
strategy,
|
||||||
|
strategy_value
|
||||||
|
});
|
||||||
|
mutator.__unstable_toggleSystemEntityCreation(true);
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
override toJSON(secrets?: boolean): AppAuthSchema {
|
override toJSON(secrets?: boolean): AppAuthSchema {
|
||||||
if (!this.config.enabled) {
|
if (!this.config.enabled) {
|
||||||
return this.configDefault;
|
return this.configDefault;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export class AuthApi extends ModuleApi<AuthApiOptions> {
|
|||||||
|
|
||||||
async loginWithPassword(input: any) {
|
async loginWithPassword(input: any) {
|
||||||
const res = await this.post<AuthResponse>(["password", "login"], input);
|
const res = await this.post<AuthResponse>(["password", "login"], input);
|
||||||
if (res.res.ok && res.body.token) {
|
if (res.ok && res.body.token) {
|
||||||
await this.options.onTokenUpdate?.(res.body.token);
|
await this.options.onTokenUpdate?.(res.body.token);
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
@@ -23,17 +23,17 @@ export class AuthApi extends ModuleApi<AuthApiOptions> {
|
|||||||
|
|
||||||
async registerWithPassword(input: any) {
|
async registerWithPassword(input: any) {
|
||||||
const res = await this.post<AuthResponse>(["password", "register"], input);
|
const res = await this.post<AuthResponse>(["password", "register"], input);
|
||||||
if (res.res.ok && res.body.token) {
|
if (res.ok && res.body.token) {
|
||||||
await this.options.onTokenUpdate?.(res.body.token);
|
await this.options.onTokenUpdate?.(res.body.token);
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
async me() {
|
me() {
|
||||||
return this.get<{ user: SafeUser | null }>(["me"]);
|
return this.get<{ user: SafeUser | null }>(["me"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async strategies() {
|
strategies() {
|
||||||
return this.get<Pick<AppAuthSchema, "strategies" | "basepath">>(["strategies"]);
|
return this.get<Pick<AppAuthSchema, "strategies" | "basepath">>(["strategies"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { AppAuth } from "auth";
|
import type { AppAuth } from "auth";
|
||||||
import type { ClassController } from "core";
|
import { type ClassController, isDebug } from "core";
|
||||||
import { Hono, type MiddlewareHandler } from "hono";
|
import { Hono, type MiddlewareHandler } from "hono";
|
||||||
|
|
||||||
export class AuthController implements ClassController {
|
export class AuthController implements ClassController {
|
||||||
@@ -10,8 +10,27 @@ export class AuthController implements ClassController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getMiddleware: MiddlewareHandler = async (c, next) => {
|
getMiddleware: MiddlewareHandler = async (c, next) => {
|
||||||
const user = await this.auth.authenticator.resolveAuthFromRequest(c);
|
// @todo: ONLY HOTFIX
|
||||||
this.auth.ctx.guard.setUserContext(user);
|
// middlewares are added for all routes are registered. But we need to make sure that
|
||||||
|
// only HTML/JSON routes are adding a cookie to the response. Config updates might
|
||||||
|
// also use an extension "syntax", e.g. /api/system/patch/data/entities.posts
|
||||||
|
// This middleware should be extracted and added by each Controller individually,
|
||||||
|
// but it requires access to the auth secret.
|
||||||
|
// Note: This doesn't mean endpoints aren't protected, just the cookie is not set.
|
||||||
|
const url = new URL(c.req.url);
|
||||||
|
const last = url.pathname.split("/")?.pop();
|
||||||
|
const ext = last?.includes(".") ? last.split(".")?.pop() : undefined;
|
||||||
|
if (
|
||||||
|
!this.auth.authenticator.isJsonRequest(c) &&
|
||||||
|
["GET", "HEAD", "OPTIONS"].includes(c.req.method) &&
|
||||||
|
ext &&
|
||||||
|
["js", "css", "png", "jpg", "jpeg", "svg", "ico"].includes(ext)
|
||||||
|
) {
|
||||||
|
isDebug() && console.log("Skipping auth", { ext }, url.pathname);
|
||||||
|
} else {
|
||||||
|
const user = await this.auth.authenticator.resolveAuthFromRequest(c);
|
||||||
|
this.auth.ctx.guard.setUserContext(user);
|
||||||
|
}
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
} from "core/utils";
|
} from "core/utils";
|
||||||
import type { Context, Hono } from "hono";
|
import type { Context, Hono } from "hono";
|
||||||
import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie";
|
import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie";
|
||||||
import { decode, sign, verify } from "hono/jwt";
|
import { sign, verify } from "hono/jwt";
|
||||||
import type { CookieOptions } from "hono/utils/cookie";
|
import type { CookieOptions } from "hono/utils/cookie";
|
||||||
import { omit } from "lodash-es";
|
import { omit } from "lodash-es";
|
||||||
|
|
||||||
@@ -177,7 +177,12 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
|||||||
payload.exp = Math.floor(Date.now() / 1000) + this.config.jwt.expires;
|
payload.exp = Math.floor(Date.now() / 1000) + this.config.jwt.expires;
|
||||||
}
|
}
|
||||||
|
|
||||||
return sign(payload, this.config.jwt?.secret ?? "", this.config.jwt?.alg ?? "HS256");
|
const secret = this.config.jwt.secret;
|
||||||
|
if (!secret || secret.length === 0) {
|
||||||
|
throw new Error("Cannot sign JWT without a secret");
|
||||||
|
}
|
||||||
|
|
||||||
|
return sign(payload, secret, this.config.jwt?.alg ?? "HS256");
|
||||||
}
|
}
|
||||||
|
|
||||||
async verify(jwt: string): Promise<boolean> {
|
async verify(jwt: string): Promise<boolean> {
|
||||||
@@ -215,22 +220,30 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getAuthCookie(c: Context): Promise<string | undefined> {
|
private async getAuthCookie(c: Context): Promise<string | undefined> {
|
||||||
const secret = this.config.jwt.secret;
|
try {
|
||||||
|
const secret = this.config.jwt.secret;
|
||||||
|
|
||||||
|
const token = await getSignedCookie(c, secret, "auth");
|
||||||
|
if (typeof token !== "string") {
|
||||||
|
await deleteCookie(c, "auth", this.cookieOptions);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return token;
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
console.error("[Error:getAuthCookie]", e.message);
|
||||||
|
}
|
||||||
|
|
||||||
const token = await getSignedCookie(c, secret, "auth");
|
|
||||||
if (typeof token !== "string") {
|
|
||||||
await deleteCookie(c, "auth", this.cookieOptions);
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return token;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async requestCookieRefresh(c: Context) {
|
async requestCookieRefresh(c: Context) {
|
||||||
if (this.config.cookie.renew) {
|
if (this.config.cookie.renew) {
|
||||||
console.log("renewing cookie", c.req.url);
|
|
||||||
const token = await this.getAuthCookie(c);
|
const token = await this.getAuthCookie(c);
|
||||||
if (token) {
|
if (token) {
|
||||||
|
console.log("renewing cookie", c.req.url);
|
||||||
await this.setAuthCookie(c, token);
|
await this.setAuthCookie(c, token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -249,6 +262,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @todo: move this to a server helper
|
||||||
isJsonRequest(c: Context): boolean {
|
isJsonRequest(c: Context): boolean {
|
||||||
//return c.req.header("Content-Type") === "application/x-www-form-urlencoded";
|
//return c.req.header("Content-Type") === "application/x-www-form-urlencoded";
|
||||||
return c.req.header("Content-Type") === "application/json";
|
return c.req.header("Content-Type") === "application/json";
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { readFile } from "node:fs/promises";
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import type { ServeStaticOptions } from "@hono/node-server/serve-static";
|
import type { Config } from "@libsql/client/node";
|
||||||
import { type Config, createClient } from "@libsql/client/node";
|
|
||||||
import { Connection, LibsqlConnection, SqliteLocalConnection } from "data";
|
|
||||||
import type { MiddlewareHandler } from "hono";
|
import type { MiddlewareHandler } from "hono";
|
||||||
import { fileExists, getDistPath, getRelativeDistPath } from "../../utils/sys";
|
import open from "open";
|
||||||
|
import { fileExists, getRelativeDistPath } from "../../utils/sys";
|
||||||
|
|
||||||
export const PLATFORMS = ["node", "bun"] as const;
|
export const PLATFORMS = ["node", "bun"] as const;
|
||||||
export type Platform = (typeof PLATFORMS)[number];
|
export type Platform = (typeof PLATFORMS)[number];
|
||||||
@@ -33,7 +31,8 @@ export async function attachServeStatic(app: any, platform: Platform) {
|
|||||||
|
|
||||||
export async function startServer(server: Platform, app: any, options: { port: number }) {
|
export async function startServer(server: Platform, app: any, options: { port: number }) {
|
||||||
const port = options.port;
|
const port = options.port;
|
||||||
console.log("running on", server, port);
|
console.log(`(using ${server} serve)`);
|
||||||
|
|
||||||
switch (server) {
|
switch (server) {
|
||||||
case "node": {
|
case "node": {
|
||||||
// https://github.com/honojs/node-server/blob/main/src/response.ts#L88
|
// https://github.com/honojs/node-server/blob/main/src/response.ts#L88
|
||||||
@@ -53,27 +52,9 @@ export async function startServer(server: Platform, app: any, options: { port: n
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Server listening on", "http://localhost:" + port);
|
const url = `http://localhost:${port}`;
|
||||||
}
|
console.log(`Server listening on ${url}`);
|
||||||
|
await open(url);
|
||||||
export async function getHtml() {
|
|
||||||
return await readFile(path.resolve(getDistPath(), "static/index.html"), "utf-8");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getConnection(connectionOrConfig?: Connection | Config): Connection {
|
|
||||||
if (connectionOrConfig) {
|
|
||||||
if (connectionOrConfig instanceof Connection) {
|
|
||||||
return connectionOrConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("url" in connectionOrConfig) {
|
|
||||||
return new LibsqlConnection(createClient(connectionOrConfig));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Using in-memory database");
|
|
||||||
return new LibsqlConnection(createClient({ url: ":memory:" }));
|
|
||||||
//return new SqliteLocalConnection(new Database(":memory:"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getConfigPath(filePath?: string) {
|
export async function getConfigPath(filePath?: string) {
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import type { Config } from "@libsql/client/node";
|
import type { Config } from "@libsql/client/node";
|
||||||
import { App } from "App";
|
import { App, type CreateAppConfig } from "App";
|
||||||
import type { BkndConfig } from "adapter";
|
import { StorageLocalAdapter } from "adapter/node";
|
||||||
import type { CliCommand } from "cli/types";
|
import type { CliBkndConfig, CliCommand } from "cli/types";
|
||||||
import { Option } from "commander";
|
import { Option } from "commander";
|
||||||
import type { Connection } from "data";
|
import { config } from "core";
|
||||||
|
import { registries } from "modules/registries";
|
||||||
import {
|
import {
|
||||||
PLATFORMS,
|
PLATFORMS,
|
||||||
type Platform,
|
type Platform,
|
||||||
attachServeStatic,
|
attachServeStatic,
|
||||||
getConfigPath,
|
getConfigPath,
|
||||||
getConnection,
|
|
||||||
getHtml,
|
|
||||||
startServer
|
startServer
|
||||||
} from "./platform";
|
} from "./platform";
|
||||||
|
|
||||||
@@ -22,7 +21,7 @@ export const run: CliCommand = (program) => {
|
|||||||
.addOption(
|
.addOption(
|
||||||
new Option("-p, --port <port>", "port to run on")
|
new Option("-p, --port <port>", "port to run on")
|
||||||
.env("PORT")
|
.env("PORT")
|
||||||
.default(1337)
|
.default(config.server.default_port)
|
||||||
.argParser((v) => Number.parseInt(v))
|
.argParser((v) => Number.parseInt(v))
|
||||||
)
|
)
|
||||||
.addOption(new Option("-c, --config <config>", "config file"))
|
.addOption(new Option("-c, --config <config>", "config file"))
|
||||||
@@ -40,18 +39,24 @@ export const run: CliCommand = (program) => {
|
|||||||
.action(action);
|
.action(action);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// automatically register local adapter
|
||||||
|
const local = StorageLocalAdapter.prototype.getName();
|
||||||
|
if (!registries.media.has(local)) {
|
||||||
|
registries.media.register(local, StorageLocalAdapter);
|
||||||
|
}
|
||||||
|
|
||||||
type MakeAppConfig = {
|
type MakeAppConfig = {
|
||||||
connection: Connection;
|
connection?: CreateAppConfig["connection"];
|
||||||
server?: { platform?: Platform };
|
server?: { platform?: Platform };
|
||||||
setAdminHtml?: boolean;
|
setAdminHtml?: boolean;
|
||||||
onBuilt?: (app: App) => Promise<void>;
|
onBuilt?: (app: App) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function makeApp(config: MakeAppConfig) {
|
async function makeApp(config: MakeAppConfig) {
|
||||||
const app = new App(config.connection);
|
const app = App.create({ connection: config.connection });
|
||||||
|
|
||||||
app.emgr.on(
|
app.emgr.onEvent(
|
||||||
"app-built",
|
App.Events.AppBuiltEvent,
|
||||||
async () => {
|
async () => {
|
||||||
await attachServeStatic(app, config.server?.platform ?? "node");
|
await attachServeStatic(app, config.server?.platform ?? "node");
|
||||||
app.registerAdminController();
|
app.registerAdminController();
|
||||||
@@ -67,24 +72,23 @@ async function makeApp(config: MakeAppConfig) {
|
|||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function makeConfigApp(config: BkndConfig, platform?: Platform) {
|
export async function makeConfigApp(config: CliBkndConfig, platform?: Platform) {
|
||||||
const appConfig = typeof config.app === "function" ? config.app(process.env) : config.app;
|
const appConfig = typeof config.app === "function" ? config.app(process.env) : config.app;
|
||||||
const app = App.create(appConfig);
|
const app = App.create(appConfig);
|
||||||
|
|
||||||
app.emgr.on(
|
app.emgr.onEvent(
|
||||||
"app-built",
|
App.Events.AppBuiltEvent,
|
||||||
async () => {
|
async () => {
|
||||||
await attachServeStatic(app, platform ?? "node");
|
await attachServeStatic(app, platform ?? "node");
|
||||||
app.registerAdminController();
|
app.registerAdminController();
|
||||||
|
|
||||||
if (config.onBuilt) {
|
await config.onBuilt?.(app);
|
||||||
await config.onBuilt(app);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"sync"
|
"sync"
|
||||||
);
|
);
|
||||||
|
|
||||||
await app.build();
|
await config.beforeBuild?.(app);
|
||||||
|
await app.build(config.buildConfig);
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,13 +103,13 @@ async function action(options: {
|
|||||||
|
|
||||||
let app: App;
|
let app: App;
|
||||||
if (options.dbUrl || !configFilePath) {
|
if (options.dbUrl || !configFilePath) {
|
||||||
const connection = getConnection(
|
const connection = options.dbUrl
|
||||||
options.dbUrl ? { url: options.dbUrl, authToken: options.dbToken } : undefined
|
? { type: "libsql" as const, config: { url: options.dbUrl, authToken: options.dbToken } }
|
||||||
);
|
: undefined;
|
||||||
app = await makeApp({ connection, server: { platform: options.server } });
|
app = await makeApp({ connection, server: { platform: options.server } });
|
||||||
} else {
|
} else {
|
||||||
console.log("Using config from:", configFilePath);
|
console.log("Using config from:", configFilePath);
|
||||||
const config = (await import(configFilePath).then((m) => m.default)) as BkndConfig;
|
const config = (await import(configFilePath).then((m) => m.default)) as CliBkndConfig;
|
||||||
app = await makeConfigApp(config, options.server);
|
app = await makeConfigApp(config, options.server);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { password as $password, text as $text } from "@clack/prompts";
|
import { password as $password, text as $text } from "@clack/prompts";
|
||||||
|
import type { App } from "App";
|
||||||
import type { PasswordStrategy } from "auth/authenticate/strategies";
|
import type { PasswordStrategy } from "auth/authenticate/strategies";
|
||||||
import type { App, BkndConfig } from "bknd";
|
|
||||||
import { makeConfigApp } from "cli/commands/run";
|
import { makeConfigApp } from "cli/commands/run";
|
||||||
import { getConfigPath } from "cli/commands/run/platform";
|
import { getConfigPath } from "cli/commands/run/platform";
|
||||||
import type { CliCommand } from "cli/types";
|
import type { CliBkndConfig, CliCommand } from "cli/types";
|
||||||
import { Argument } from "commander";
|
import { Argument } from "commander";
|
||||||
|
|
||||||
export const user: CliCommand = (program) => {
|
export const user: CliCommand = (program) => {
|
||||||
@@ -21,7 +21,7 @@ async function action(action: "create" | "update", options: any) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = (await import(configFilePath).then((m) => m.default)) as BkndConfig;
|
const config = (await import(configFilePath).then((m) => m.default)) as CliBkndConfig;
|
||||||
const app = await makeConfigApp(config, options.server);
|
const app = await makeConfigApp(config, options.server);
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
@@ -37,7 +37,7 @@ async function action(action: "create" | "update", options: any) {
|
|||||||
async function create(app: App, options: any) {
|
async function create(app: App, options: any) {
|
||||||
const config = app.module.auth.toJSON(true);
|
const config = app.module.auth.toJSON(true);
|
||||||
const strategy = app.module.auth.authenticator.strategy("password") as PasswordStrategy;
|
const strategy = app.module.auth.authenticator.strategy("password") as PasswordStrategy;
|
||||||
const users_entity = config.entity_name;
|
const users_entity = config.entity_name as "users";
|
||||||
|
|
||||||
const email = await $text({
|
const email = await $text({
|
||||||
message: "Enter email",
|
message: "Enter email",
|
||||||
@@ -83,7 +83,7 @@ async function create(app: App, options: any) {
|
|||||||
async function update(app: App, options: any) {
|
async function update(app: App, options: any) {
|
||||||
const config = app.module.auth.toJSON(true);
|
const config = app.module.auth.toJSON(true);
|
||||||
const strategy = app.module.auth.authenticator.strategy("password") as PasswordStrategy;
|
const strategy = app.module.auth.authenticator.strategy("password") as PasswordStrategy;
|
||||||
const users_entity = config.entity_name;
|
const users_entity = config.entity_name as "users";
|
||||||
const em = app.modules.ctx().em;
|
const em = app.modules.ctx().em;
|
||||||
|
|
||||||
const email = (await $text({
|
const email = (await $text({
|
||||||
|
|||||||
Vendored
+11
@@ -1,3 +1,14 @@
|
|||||||
|
import type { CreateAppConfig } from "App";
|
||||||
|
import type { FrameworkBkndConfig } from "adapter";
|
||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
|
|
||||||
export type CliCommand = (program: Command) => void;
|
export type CliCommand = (program: Command) => void;
|
||||||
|
|
||||||
|
export type CliBkndConfig<Env = any> = FrameworkBkndConfig & {
|
||||||
|
app: CreateAppConfig | ((env: Env) => CreateAppConfig);
|
||||||
|
setAdminHtml?: boolean;
|
||||||
|
server?: {
|
||||||
|
port?: number;
|
||||||
|
platform?: "node" | "bun";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,7 +5,13 @@ import type { Generated } from "kysely";
|
|||||||
|
|
||||||
export type PrimaryFieldType = number | Generated<number>;
|
export type PrimaryFieldType = number | Generated<number>;
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/noEmptyInterface: <explanation>
|
||||||
|
export interface DB {}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
|
server: {
|
||||||
|
default_port: 1337
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
default_primary_field: "id"
|
default_primary_field: "id"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ export class BkndError extends Error {
|
|||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static with(message: string, details?: Record<string, any>, type?: string) {
|
||||||
|
throw new BkndError(message, details, type);
|
||||||
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
type: this.type ?? "unknown",
|
type: this.type ?? "unknown",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export class EventManager<
|
|||||||
> {
|
> {
|
||||||
protected events: EventClass[] = [];
|
protected events: EventClass[] = [];
|
||||||
protected listeners: EventListener[] = [];
|
protected listeners: EventListener[] = [];
|
||||||
|
enabled: boolean = true;
|
||||||
|
|
||||||
constructor(events?: RegisteredEvents, listeners?: EventListener[]) {
|
constructor(events?: RegisteredEvents, listeners?: EventListener[]) {
|
||||||
if (events) {
|
if (events) {
|
||||||
@@ -28,6 +29,16 @@ export class EventManager<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enable() {
|
||||||
|
this.enabled = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
this.enabled = false;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
clearEvents() {
|
clearEvents() {
|
||||||
this.events = [];
|
this.events = [];
|
||||||
return this;
|
return this;
|
||||||
@@ -39,6 +50,10 @@ export class EventManager<
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getListeners(): EventListener[] {
|
||||||
|
return [...this.listeners];
|
||||||
|
}
|
||||||
|
|
||||||
get Events(): { [K in keyof RegisteredEvents]: RegisteredEvents[K] } {
|
get Events(): { [K in keyof RegisteredEvents]: RegisteredEvents[K] } {
|
||||||
// proxy class to access events
|
// proxy class to access events
|
||||||
return new Proxy(this, {
|
return new Proxy(this, {
|
||||||
@@ -133,6 +148,11 @@ export class EventManager<
|
|||||||
async emit(event: Event) {
|
async emit(event: Event) {
|
||||||
// @ts-expect-error slug is static
|
// @ts-expect-error slug is static
|
||||||
const slug = event.constructor.slug;
|
const slug = event.constructor.slug;
|
||||||
|
if (!this.enabled) {
|
||||||
|
console.log("EventManager disabled, not emitting", slug);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.eventExists(event)) {
|
if (!this.eventExists(event)) {
|
||||||
throw new Error(`Event "${slug}" not registered`);
|
throw new Error(`Event "${slug}" not registered`);
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-4
@@ -1,9 +1,9 @@
|
|||||||
export { Endpoint, type RequestResponse, type Middleware } from "./server/Endpoint";
|
import type { Hono, MiddlewareHandler } from "hono";
|
||||||
export { zValidator } from "./server/lib/zValidator";
|
|
||||||
export { tbValidator } from "./server/lib/tbValidator";
|
export { tbValidator } from "./server/lib/tbValidator";
|
||||||
export { Exception, BkndError } from "./errors";
|
export { Exception, BkndError } from "./errors";
|
||||||
export { isDebug } from "./env";
|
export { isDebug } from "./env";
|
||||||
export { type PrimaryFieldType, config } from "./config";
|
export { type PrimaryFieldType, config, type DB } from "./config";
|
||||||
export { AwsClient } from "./clients/aws/AwsClient";
|
export { AwsClient } from "./clients/aws/AwsClient";
|
||||||
export {
|
export {
|
||||||
SimpleRenderer,
|
SimpleRenderer,
|
||||||
@@ -11,7 +11,6 @@ export {
|
|||||||
type TemplateTypes,
|
type TemplateTypes,
|
||||||
type SimpleRendererOptions
|
type SimpleRendererOptions
|
||||||
} from "./template/SimpleRenderer";
|
} from "./template/SimpleRenderer";
|
||||||
export { Controller, type ClassController } from "./server/Controller";
|
|
||||||
export { SchemaObject } from "./object/SchemaObject";
|
export { SchemaObject } from "./object/SchemaObject";
|
||||||
export { DebugLogger } from "./utils/DebugLogger";
|
export { DebugLogger } from "./utils/DebugLogger";
|
||||||
export { Permission } from "./security/Permission";
|
export { Permission } from "./security/Permission";
|
||||||
@@ -26,3 +25,10 @@ export {
|
|||||||
isBooleanLike
|
isBooleanLike
|
||||||
} from "./object/query/query";
|
} from "./object/query/query";
|
||||||
export { Registry, type Constructor } from "./registry/Registry";
|
export { Registry, type Constructor } from "./registry/Registry";
|
||||||
|
|
||||||
|
// compatibility
|
||||||
|
export type Middleware = MiddlewareHandler<any, any, any>;
|
||||||
|
export interface ClassController {
|
||||||
|
getController: () => Hono<any, any, any>;
|
||||||
|
getMiddleware?: MiddlewareHandler<any, any, any>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ export class SchemaObject<Schema extends TObject> {
|
|||||||
forceParse: true,
|
forceParse: true,
|
||||||
skipMark: this.isForceParse()
|
skipMark: this.isForceParse()
|
||||||
});
|
});
|
||||||
const updatedConfig = noEmit ? valid : await this.onBeforeUpdate(this._config, valid);
|
// regardless of "noEmit" – this should always be triggered
|
||||||
|
const updatedConfig = await this.onBeforeUpdate(this._config, valid);
|
||||||
|
|
||||||
this._value = updatedConfig;
|
this._value = updatedConfig;
|
||||||
this._config = Object.freeze(updatedConfig);
|
this._config = Object.freeze(updatedConfig);
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
enum Change {
|
||||||
|
Add = "a",
|
||||||
|
Remove = "r",
|
||||||
|
Edit = "e"
|
||||||
|
}
|
||||||
|
|
||||||
|
type Object = object;
|
||||||
|
type Primitive = string | number | boolean | null | object | any[] | undefined;
|
||||||
|
|
||||||
|
interface DiffEntry {
|
||||||
|
t: Change | string;
|
||||||
|
p: (string | number)[];
|
||||||
|
o: Primitive;
|
||||||
|
n: Primitive;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isObject(value: any): value is Object {
|
||||||
|
return value !== null && value.constructor.name === "Object";
|
||||||
|
}
|
||||||
|
function isPrimitive(value: any): value is Primitive {
|
||||||
|
try {
|
||||||
|
return (
|
||||||
|
value === null ||
|
||||||
|
value === undefined ||
|
||||||
|
typeof value === "string" ||
|
||||||
|
typeof value === "number" ||
|
||||||
|
typeof value === "boolean" ||
|
||||||
|
Array.isArray(value) ||
|
||||||
|
isObject(value)
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function diff(oldObj: Object, newObj: Object): DiffEntry[] {
|
||||||
|
const diffs: DiffEntry[] = [];
|
||||||
|
|
||||||
|
function recurse(oldValue: Primitive, newValue: Primitive, path: (string | number)[]) {
|
||||||
|
if (!isPrimitive(oldValue) || !isPrimitive(newValue)) {
|
||||||
|
throw new Error("Diff: Only primitive types are supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldValue === newValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof oldValue !== typeof newValue) {
|
||||||
|
diffs.push({
|
||||||
|
t: Change.Edit,
|
||||||
|
p: path,
|
||||||
|
o: oldValue,
|
||||||
|
n: newValue
|
||||||
|
});
|
||||||
|
} else if (Array.isArray(oldValue) && Array.isArray(newValue)) {
|
||||||
|
const maxLength = Math.max(oldValue.length, newValue.length);
|
||||||
|
for (let i = 0; i < maxLength; i++) {
|
||||||
|
if (i >= oldValue.length) {
|
||||||
|
diffs.push({
|
||||||
|
t: Change.Add,
|
||||||
|
p: [...path, i],
|
||||||
|
o: undefined,
|
||||||
|
n: newValue[i]
|
||||||
|
});
|
||||||
|
} else if (i >= newValue.length) {
|
||||||
|
diffs.push({
|
||||||
|
t: Change.Remove,
|
||||||
|
p: [...path, i],
|
||||||
|
o: oldValue[i],
|
||||||
|
n: undefined
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
recurse(oldValue[i], newValue[i], [...path, i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (isObject(oldValue) && isObject(newValue)) {
|
||||||
|
const oKeys = Object.keys(oldValue);
|
||||||
|
const nKeys = Object.keys(newValue);
|
||||||
|
const allKeys = new Set([...oKeys, ...nKeys]);
|
||||||
|
for (const key of allKeys) {
|
||||||
|
if (!(key in oldValue)) {
|
||||||
|
diffs.push({
|
||||||
|
t: Change.Add,
|
||||||
|
p: [...path, key],
|
||||||
|
o: undefined,
|
||||||
|
n: newValue[key]
|
||||||
|
});
|
||||||
|
} else if (!(key in newValue)) {
|
||||||
|
diffs.push({
|
||||||
|
t: Change.Remove,
|
||||||
|
p: [...path, key],
|
||||||
|
o: oldValue[key],
|
||||||
|
n: undefined
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
recurse(oldValue[key], newValue[key], [...path, key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
diffs.push({
|
||||||
|
t: Change.Edit,
|
||||||
|
p: path,
|
||||||
|
o: oldValue,
|
||||||
|
n: newValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recurse(oldObj, newObj, []);
|
||||||
|
return diffs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function apply(obj: Object, diffs: DiffEntry[]): any {
|
||||||
|
const clonedObj = clone(obj);
|
||||||
|
|
||||||
|
for (const diff of diffs) {
|
||||||
|
applyChange(clonedObj, diff);
|
||||||
|
}
|
||||||
|
|
||||||
|
return clonedObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
function revert(obj: Object, diffs: DiffEntry[]): any {
|
||||||
|
const clonedObj = clone(obj);
|
||||||
|
const reversedDiffs = diffs.slice().reverse();
|
||||||
|
|
||||||
|
for (const diff of reversedDiffs) {
|
||||||
|
revertChange(clonedObj, diff);
|
||||||
|
}
|
||||||
|
|
||||||
|
return clonedObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyChange(obj: Object, diff: DiffEntry) {
|
||||||
|
const { p: path, t: type, n: newValue } = diff;
|
||||||
|
const parent = getParent(obj, path.slice(0, -1));
|
||||||
|
const key = path[path.length - 1]!;
|
||||||
|
|
||||||
|
if (type === Change.Add || type === Change.Edit) {
|
||||||
|
parent[key] = newValue;
|
||||||
|
} else if (type === Change.Remove) {
|
||||||
|
if (Array.isArray(parent)) {
|
||||||
|
parent.splice(key as number, 1);
|
||||||
|
} else {
|
||||||
|
delete parent[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function revertChange(obj: Object, diff: DiffEntry) {
|
||||||
|
const { p: path, t: type, o: oldValue } = diff;
|
||||||
|
const parent = getParent(obj, path.slice(0, -1));
|
||||||
|
const key = path[path.length - 1]!;
|
||||||
|
|
||||||
|
if (type === Change.Add) {
|
||||||
|
if (Array.isArray(parent)) {
|
||||||
|
parent.splice(key as number, 1);
|
||||||
|
} else {
|
||||||
|
delete parent[key];
|
||||||
|
}
|
||||||
|
} else if (type === Change.Remove || type === Change.Edit) {
|
||||||
|
parent[key] = oldValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getParent(obj: Object, path: (string | number)[]): any {
|
||||||
|
let current = obj;
|
||||||
|
for (const key of path) {
|
||||||
|
if (current[key] === undefined) {
|
||||||
|
current[key] = typeof key === "number" ? [] : {};
|
||||||
|
}
|
||||||
|
current = current[key];
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clone<In extends Object>(obj: In): In {
|
||||||
|
return JSON.parse(JSON.stringify(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
export { diff, apply, revert, clone };
|
||||||
@@ -1,29 +1,50 @@
|
|||||||
export type Constructor<T> = new (...args: any[]) => T;
|
export type Constructor<T> = new (...args: any[]) => T;
|
||||||
export class Registry<Item, Items extends Record<string, object> = Record<string, object>> {
|
|
||||||
|
export type RegisterFn<Item> = (unknown: any) => Item;
|
||||||
|
|
||||||
|
export class Registry<
|
||||||
|
Item,
|
||||||
|
Items extends Record<string, Item> = Record<string, Item>,
|
||||||
|
Fn extends RegisterFn<Item> = RegisterFn<Item>
|
||||||
|
> {
|
||||||
private is_set: boolean = false;
|
private is_set: boolean = false;
|
||||||
private items: Items = {} as Items;
|
private items: Items = {} as Items;
|
||||||
|
|
||||||
set<Actual extends Record<string, object>>(items: Actual) {
|
constructor(private registerFn?: Fn) {}
|
||||||
|
|
||||||
|
set<Actual extends Record<string, Item>>(items: Actual) {
|
||||||
if (this.is_set) {
|
if (this.is_set) {
|
||||||
throw new Error("Registry is already set");
|
throw new Error("Registry is already set");
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
this.items = items as unknown as Items;
|
||||||
this.items = items;
|
|
||||||
this.is_set = true;
|
this.is_set = true;
|
||||||
|
|
||||||
return this as unknown as Registry<Item, Actual>;
|
return this as unknown as Registry<Item, Actual, Fn>;
|
||||||
}
|
}
|
||||||
|
|
||||||
add(name: string, item: Item) {
|
add(name: string, item: Item) {
|
||||||
// @ts-ignore
|
this.items[name as keyof Items] = item as Items[keyof Items];
|
||||||
this.items[name] = item;
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
register(name: string, specific: Parameters<Fn>[0]) {
|
||||||
|
if (this.registerFn) {
|
||||||
|
const item = this.registerFn(specific);
|
||||||
|
this.items[name as keyof Items] = item as Items[keyof Items];
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.add(name, specific);
|
||||||
|
}
|
||||||
|
|
||||||
get<Name extends keyof Items>(name: Name): Items[Name] {
|
get<Name extends keyof Items>(name: Name): Items[Name] {
|
||||||
return this.items[name];
|
return this.items[name];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
has(name: keyof Items): boolean {
|
||||||
|
return name in this.items;
|
||||||
|
}
|
||||||
|
|
||||||
all() {
|
all() {
|
||||||
return this.items;
|
return this.items;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,155 +0,0 @@
|
|||||||
import { Hono, type MiddlewareHandler, type ValidationTargets } from "hono";
|
|
||||||
import type { H } from "hono/types";
|
|
||||||
import { safelyParseObjectValues } from "../utils";
|
|
||||||
import type { Endpoint, Middleware } from "./Endpoint";
|
|
||||||
import { zValidator } from "./lib/zValidator";
|
|
||||||
|
|
||||||
type RouteProxy<Endpoints> = {
|
|
||||||
[K in keyof Endpoints]: Endpoints[K];
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface ClassController {
|
|
||||||
getController: () => Hono<any, any, any>;
|
|
||||||
getMiddleware?: MiddlewareHandler<any, any, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
*/
|
|
||||||
export class Controller<
|
|
||||||
Endpoints extends Record<string, Endpoint> = Record<string, Endpoint>,
|
|
||||||
Middlewares extends Record<string, Middleware> = Record<string, Middleware>
|
|
||||||
> {
|
|
||||||
protected endpoints: Endpoints = {} as Endpoints;
|
|
||||||
protected middlewares: Middlewares = {} as Middlewares;
|
|
||||||
|
|
||||||
public prefix: string = "/";
|
|
||||||
public routes: RouteProxy<Endpoints>;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
prefix: string = "/",
|
|
||||||
endpoints: Endpoints = {} as Endpoints,
|
|
||||||
middlewares: Middlewares = {} as Middlewares
|
|
||||||
) {
|
|
||||||
this.prefix = prefix;
|
|
||||||
this.endpoints = endpoints;
|
|
||||||
this.middlewares = middlewares;
|
|
||||||
|
|
||||||
this.routes = new Proxy(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
get: (_, name: string) => {
|
|
||||||
return this.endpoints[name];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) as RouteProxy<Endpoints>;
|
|
||||||
}
|
|
||||||
|
|
||||||
add<Name extends string, E extends Endpoint>(
|
|
||||||
this: Controller<Endpoints>,
|
|
||||||
name: Name,
|
|
||||||
endpoint: E
|
|
||||||
): Controller<Endpoints & Record<Name, E>> {
|
|
||||||
const newEndpoints = {
|
|
||||||
...this.endpoints,
|
|
||||||
[name]: endpoint
|
|
||||||
} as Endpoints & Record<Name, E>;
|
|
||||||
const newController: Controller<Endpoints & Record<Name, E>> = new Controller<
|
|
||||||
Endpoints & Record<Name, E>
|
|
||||||
>();
|
|
||||||
newController.endpoints = newEndpoints;
|
|
||||||
newController.middlewares = this.middlewares;
|
|
||||||
return newController;
|
|
||||||
}
|
|
||||||
|
|
||||||
get<Name extends keyof Endpoints>(name: Name): Endpoints[Name] {
|
|
||||||
return this.endpoints[name];
|
|
||||||
}
|
|
||||||
|
|
||||||
honoify(_hono: Hono = new Hono()) {
|
|
||||||
const hono = _hono.basePath(this.prefix);
|
|
||||||
|
|
||||||
// apply middlewares
|
|
||||||
for (const m_name in this.middlewares) {
|
|
||||||
const middleware = this.middlewares[m_name];
|
|
||||||
|
|
||||||
if (typeof middleware === "function") {
|
|
||||||
//if (isDebug()) console.log("+++ appyling middleware", m_name, middleware);
|
|
||||||
hono.use(middleware);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// apply endpoints
|
|
||||||
for (const name in this.endpoints) {
|
|
||||||
const endpoint = this.endpoints[name];
|
|
||||||
if (!endpoint) continue;
|
|
||||||
|
|
||||||
const handlers: H[] = [];
|
|
||||||
|
|
||||||
const supportedValidations: Array<keyof ValidationTargets> = ["param", "query", "json"];
|
|
||||||
|
|
||||||
// if validations are present, add them to the handlers
|
|
||||||
for (const validation of supportedValidations) {
|
|
||||||
if (endpoint.validation[validation]) {
|
|
||||||
handlers.push(async (c, next) => {
|
|
||||||
// @todo: potentially add "strict" to all schemas?
|
|
||||||
const res = await zValidator(
|
|
||||||
validation,
|
|
||||||
endpoint.validation[validation] as any,
|
|
||||||
(target, value, c) => {
|
|
||||||
if (["query", "param"].includes(target)) {
|
|
||||||
return safelyParseObjectValues(value);
|
|
||||||
}
|
|
||||||
//console.log("preprocess", target, value, c.req.raw.url);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
)(c, next);
|
|
||||||
|
|
||||||
if (res instanceof Response && res.status === 400) {
|
|
||||||
const error = await res.json();
|
|
||||||
return c.json(
|
|
||||||
{
|
|
||||||
error: "Validation error",
|
|
||||||
target: validation,
|
|
||||||
message: error
|
|
||||||
},
|
|
||||||
400
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add actual handler
|
|
||||||
handlers.push(endpoint.toHandler());
|
|
||||||
|
|
||||||
const method = endpoint.method.toLowerCase() as
|
|
||||||
| "get"
|
|
||||||
| "post"
|
|
||||||
| "put"
|
|
||||||
| "delete"
|
|
||||||
| "patch";
|
|
||||||
|
|
||||||
//if (isDebug()) console.log("--- adding", method, endpoint.path);
|
|
||||||
hono[method](endpoint.path, ...handlers);
|
|
||||||
}
|
|
||||||
|
|
||||||
return hono;
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON() {
|
|
||||||
const endpoints: any = {};
|
|
||||||
for (const name in this.endpoints) {
|
|
||||||
const endpoint = this.endpoints[name];
|
|
||||||
if (!endpoint) continue;
|
|
||||||
|
|
||||||
endpoints[name] = {
|
|
||||||
method: endpoint.method,
|
|
||||||
path: (this.prefix + endpoint.path).replace("//", "/")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return endpoints;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
import type { Context, MiddlewareHandler, Next, ValidationTargets } from "hono";
|
|
||||||
import type { Handler } from "hono/types";
|
|
||||||
import { encodeSearch, replaceUrlParam } from "../utils";
|
|
||||||
import type { Prettify } from "../utils";
|
|
||||||
|
|
||||||
type ZodSchema = { [key: string]: any };
|
|
||||||
|
|
||||||
type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
|
||||||
type Validation<P, Q, B> = {
|
|
||||||
[K in keyof ValidationTargets]?: any;
|
|
||||||
} & {
|
|
||||||
param?: P extends ZodSchema ? P : undefined;
|
|
||||||
query?: Q extends ZodSchema ? Q : undefined;
|
|
||||||
json?: B extends ZodSchema ? B : undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ValidationInput<P, Q, B> = {
|
|
||||||
param?: P extends ZodSchema ? P["_input"] : undefined;
|
|
||||||
query?: Q extends ZodSchema ? Q["_input"] : undefined;
|
|
||||||
json?: B extends ZodSchema ? B["_input"] : undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
type HonoEnv = any;
|
|
||||||
|
|
||||||
export type Middleware = MiddlewareHandler<any, any, any>;
|
|
||||||
|
|
||||||
type HandlerFunction<P extends string, R> = (c: Context<HonoEnv, P, any>, next: Next) => R;
|
|
||||||
export type RequestResponse<R> = {
|
|
||||||
status: number;
|
|
||||||
ok: boolean;
|
|
||||||
response: Awaited<R>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
*/
|
|
||||||
export class Endpoint<
|
|
||||||
Path extends string = any,
|
|
||||||
P extends ZodSchema = any,
|
|
||||||
Q extends ZodSchema = any,
|
|
||||||
B extends ZodSchema = any,
|
|
||||||
R = any
|
|
||||||
> {
|
|
||||||
constructor(
|
|
||||||
readonly method: Method,
|
|
||||||
readonly path: Path,
|
|
||||||
readonly handler: HandlerFunction<Path, R>,
|
|
||||||
readonly validation: Validation<P, Q, B> = {}
|
|
||||||
) {}
|
|
||||||
|
|
||||||
// @todo: typing is not ideal
|
|
||||||
async $request(
|
|
||||||
args?: ValidationInput<P, Q, B>,
|
|
||||||
baseUrl: string = "http://localhost:28623"
|
|
||||||
): Promise<Prettify<RequestResponse<R>>> {
|
|
||||||
let path = this.path as string;
|
|
||||||
if (args?.param) {
|
|
||||||
path = replaceUrlParam(path, args.param);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args?.query) {
|
|
||||||
path += "?" + encodeSearch(args.query);
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = [baseUrl, path].join("").replace(/\/$/, "");
|
|
||||||
const options: RequestInit = {
|
|
||||||
method: this.method,
|
|
||||||
headers: {} as any
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!["GET", "HEAD"].includes(this.method)) {
|
|
||||||
if (args?.json) {
|
|
||||||
options.body = JSON.stringify(args.json);
|
|
||||||
options.headers!["Content-Type"] = "application/json";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetch(url, options);
|
|
||||||
return {
|
|
||||||
status: res.status,
|
|
||||||
ok: res.ok,
|
|
||||||
response: (await res.json()) as any
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
toHandler(): Handler {
|
|
||||||
return async (c, next) => {
|
|
||||||
const res = await this.handler(c, next);
|
|
||||||
//console.log("toHandler:isResponse", res instanceof Response);
|
|
||||||
//return res;
|
|
||||||
if (res instanceof Response) {
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
return c.json(res as any) as unknown as Handler;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static get<
|
|
||||||
Path extends string = any,
|
|
||||||
P extends ZodSchema = any,
|
|
||||||
Q extends ZodSchema = any,
|
|
||||||
B extends ZodSchema = any,
|
|
||||||
R = any
|
|
||||||
>(path: Path, handler: HandlerFunction<Path, R>, validation?: Validation<P, Q, B>) {
|
|
||||||
return new Endpoint<Path, P, Q, B, R>("GET", path, handler, validation);
|
|
||||||
}
|
|
||||||
|
|
||||||
static post<
|
|
||||||
Path extends string = any,
|
|
||||||
P extends ZodSchema = any,
|
|
||||||
Q extends ZodSchema = any,
|
|
||||||
B extends ZodSchema = any,
|
|
||||||
R = any
|
|
||||||
>(path: Path, handler: HandlerFunction<Path, R>, validation?: Validation<P, Q, B>) {
|
|
||||||
return new Endpoint<Path, P, Q, B, R>("POST", path, handler, validation);
|
|
||||||
}
|
|
||||||
|
|
||||||
static patch<
|
|
||||||
Path extends string = any,
|
|
||||||
P extends ZodSchema = any,
|
|
||||||
Q extends ZodSchema = any,
|
|
||||||
B extends ZodSchema = any,
|
|
||||||
R = any
|
|
||||||
>(path: Path, handler: HandlerFunction<Path, R>, validation?: Validation<P, Q, B>) {
|
|
||||||
return new Endpoint<Path, P, Q, B, R>("PATCH", path, handler, validation);
|
|
||||||
}
|
|
||||||
|
|
||||||
static put<
|
|
||||||
Path extends string = any,
|
|
||||||
P extends ZodSchema = any,
|
|
||||||
Q extends ZodSchema = any,
|
|
||||||
B extends ZodSchema = any,
|
|
||||||
R = any
|
|
||||||
>(path: Path, handler: HandlerFunction<Path, R>, validation?: Validation<P, Q, B>) {
|
|
||||||
return new Endpoint<Path, P, Q, B, R>("PUT", path, handler, validation);
|
|
||||||
}
|
|
||||||
|
|
||||||
static delete<
|
|
||||||
Path extends string = any,
|
|
||||||
P extends ZodSchema = any,
|
|
||||||
Q extends ZodSchema = any,
|
|
||||||
B extends ZodSchema = any,
|
|
||||||
R = any
|
|
||||||
>(path: Path, handler: HandlerFunction<Path, R>, validation?: Validation<P, Q, B>) {
|
|
||||||
return new Endpoint<Path, P, Q, B, R>("DELETE", path, handler, validation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import type {
|
|
||||||
Context,
|
|
||||||
Env,
|
|
||||||
Input,
|
|
||||||
MiddlewareHandler,
|
|
||||||
TypedResponse,
|
|
||||||
ValidationTargets,
|
|
||||||
} from "hono";
|
|
||||||
import { validator } from "hono/validator";
|
|
||||||
import type { ZodError, ZodSchema, z } from "zod";
|
|
||||||
|
|
||||||
export type Hook<T, E extends Env, P extends string, O = {}> = (
|
|
||||||
result: { success: true; data: T } | { success: false; error: ZodError; data: T },
|
|
||||||
c: Context<E, P>,
|
|
||||||
) => Response | void | TypedResponse<O> | Promise<Response | void | TypedResponse<O>>;
|
|
||||||
|
|
||||||
type HasUndefined<T> = undefined extends T ? true : false;
|
|
||||||
|
|
||||||
export const zValidator = <
|
|
||||||
T extends ZodSchema,
|
|
||||||
Target extends keyof ValidationTargets,
|
|
||||||
E extends Env,
|
|
||||||
P extends string,
|
|
||||||
In = z.input<T>,
|
|
||||||
Out = z.output<T>,
|
|
||||||
I extends Input = {
|
|
||||||
in: HasUndefined<In> extends true
|
|
||||||
? {
|
|
||||||
[K in Target]?: K extends "json"
|
|
||||||
? In
|
|
||||||
: HasUndefined<keyof ValidationTargets[K]> extends true
|
|
||||||
? { [K2 in keyof In]?: ValidationTargets[K][K2] }
|
|
||||||
: { [K2 in keyof In]: ValidationTargets[K][K2] };
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
[K in Target]: K extends "json"
|
|
||||||
? In
|
|
||||||
: HasUndefined<keyof ValidationTargets[K]> extends true
|
|
||||||
? { [K2 in keyof In]?: ValidationTargets[K][K2] }
|
|
||||||
: { [K2 in keyof In]: ValidationTargets[K][K2] };
|
|
||||||
};
|
|
||||||
out: { [K in Target]: Out };
|
|
||||||
},
|
|
||||||
V extends I = I,
|
|
||||||
>(
|
|
||||||
target: Target,
|
|
||||||
schema: T,
|
|
||||||
preprocess?: (target: string, value: In, c: Context<E, P>) => V, // <-- added
|
|
||||||
hook?: Hook<z.infer<T>, E, P>,
|
|
||||||
): MiddlewareHandler<E, P, V> =>
|
|
||||||
// @ts-expect-error not typed well
|
|
||||||
validator(target, async (value, c) => {
|
|
||||||
// added: preprocess value first if given
|
|
||||||
const _value = preprocess ? preprocess(target, value, c) : (value as any);
|
|
||||||
const result = await schema.safeParseAsync(_value);
|
|
||||||
|
|
||||||
if (hook) {
|
|
||||||
const hookResult = await hook({ data: value, ...result }, c);
|
|
||||||
if (hookResult) {
|
|
||||||
if (hookResult instanceof Response) {
|
|
||||||
return hookResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("response" in hookResult) {
|
|
||||||
return hookResult.response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
return c.json(result, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.data as z.infer<T>;
|
|
||||||
});
|
|
||||||
@@ -20,11 +20,16 @@ export class DebugLogger {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.last = 0;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
log(...args: any[]) {
|
log(...args: any[]) {
|
||||||
if (!this._enabled) return this;
|
if (!this._enabled) return this;
|
||||||
|
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
const time = Number.parseInt(String(now - this.last));
|
const time = this.last === 0 ? 0 : Number.parseInt(String(now - this.last));
|
||||||
const indents = " ".repeat(this._context.length);
|
const indents = " ".repeat(this._context.length);
|
||||||
const context =
|
const context =
|
||||||
this._context.length > 0 ? `[${this._context[this._context.length - 1]}]` : "";
|
this._context.length > 0 ? `[${this._context[this._context.length - 1]}]` : "";
|
||||||
|
|||||||
@@ -5,10 +5,36 @@ const _oldConsoles = {
|
|||||||
error: console.error
|
error: console.error
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export async function withDisabledConsole<R>(
|
||||||
|
fn: () => Promise<R>,
|
||||||
|
severities: ConsoleSeverity[] = ["log"]
|
||||||
|
): Promise<R> {
|
||||||
|
const _oldConsoles = {
|
||||||
|
log: console.log,
|
||||||
|
warn: console.warn,
|
||||||
|
error: console.error
|
||||||
|
};
|
||||||
|
disableConsoleLog(severities);
|
||||||
|
const enable = () => {
|
||||||
|
Object.entries(_oldConsoles).forEach(([severity, fn]) => {
|
||||||
|
console[severity as ConsoleSeverity] = fn;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const result = await fn();
|
||||||
|
enable();
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
enable();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function disableConsoleLog(severities: ConsoleSeverity[] = ["log"]) {
|
export function disableConsoleLog(severities: ConsoleSeverity[] = ["log"]) {
|
||||||
severities.forEach((severity) => {
|
severities.forEach((severity) => {
|
||||||
console[severity] = () => null;
|
console[severity] = () => null;
|
||||||
});
|
});
|
||||||
|
return enableConsoleLog;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function enableConsoleLog() {
|
export function enableConsoleLog() {
|
||||||
|
|||||||
@@ -72,10 +72,10 @@ export class TypeInvalidError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stripMark(obj: any) {
|
export function stripMark<O = any>(obj: O) {
|
||||||
const newObj = cloneDeep(obj);
|
const newObj = cloneDeep(obj);
|
||||||
mark(newObj, false);
|
mark(newObj, false);
|
||||||
return newObj;
|
return newObj as O;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mark(obj: any, validated = true) {
|
export function mark(obj: any, validated = true) {
|
||||||
|
|||||||
+13
-45
@@ -1,52 +1,20 @@
|
|||||||
import { transformObject } from "core/utils";
|
import { transformObject } from "core/utils";
|
||||||
import { DataPermissions, Entity, EntityIndex, type EntityManager, type Field } from "data";
|
import {
|
||||||
|
DataPermissions,
|
||||||
|
type Entity,
|
||||||
|
EntityIndex,
|
||||||
|
type EntityManager,
|
||||||
|
constructEntity,
|
||||||
|
constructRelation
|
||||||
|
} from "data";
|
||||||
import { Module } from "modules/Module";
|
import { Module } from "modules/Module";
|
||||||
import { DataController } from "./api/DataController";
|
import { DataController } from "./api/DataController";
|
||||||
import {
|
import { type AppDataConfig, dataConfigSchema } from "./data-schema";
|
||||||
type AppDataConfig,
|
|
||||||
FIELDS,
|
|
||||||
RELATIONS,
|
|
||||||
type TAppDataEntity,
|
|
||||||
type TAppDataRelation,
|
|
||||||
dataConfigSchema
|
|
||||||
} from "./data-schema";
|
|
||||||
|
|
||||||
export class AppData<DB> extends Module<typeof dataConfigSchema> {
|
|
||||||
static constructEntity(name: string, entityConfig: TAppDataEntity) {
|
|
||||||
const fields = transformObject(entityConfig.fields ?? {}, (fieldConfig, name) => {
|
|
||||||
const { type } = fieldConfig;
|
|
||||||
if (!(type in FIELDS)) {
|
|
||||||
throw new Error(`Field type "${type}" not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { field } = FIELDS[type as any];
|
|
||||||
const returnal = new field(name, fieldConfig.config) as Field;
|
|
||||||
return returnal;
|
|
||||||
});
|
|
||||||
|
|
||||||
// @todo: entity must be migrated to typebox
|
|
||||||
return new Entity(
|
|
||||||
name,
|
|
||||||
Object.values(fields),
|
|
||||||
entityConfig.config as any,
|
|
||||||
entityConfig.type as any
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static constructRelation(
|
|
||||||
relationConfig: TAppDataRelation,
|
|
||||||
resolver: (name: Entity | string) => Entity
|
|
||||||
) {
|
|
||||||
return new RELATIONS[relationConfig.type].cls(
|
|
||||||
resolver(relationConfig.source),
|
|
||||||
resolver(relationConfig.target),
|
|
||||||
relationConfig.config
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export class AppData extends Module<typeof dataConfigSchema> {
|
||||||
override async build() {
|
override async build() {
|
||||||
const entities = transformObject(this.config.entities ?? {}, (entityConfig, name) => {
|
const entities = transformObject(this.config.entities ?? {}, (entityConfig, name) => {
|
||||||
return AppData.constructEntity(name, entityConfig);
|
return constructEntity(name, entityConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
const _entity = (_e: Entity | string): Entity => {
|
const _entity = (_e: Entity | string): Entity => {
|
||||||
@@ -57,7 +25,7 @@ export class AppData<DB> extends Module<typeof dataConfigSchema> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const relations = transformObject(this.config.relations ?? {}, (relation) =>
|
const relations = transformObject(this.config.relations ?? {}, (relation) =>
|
||||||
AppData.constructRelation(relation, _entity)
|
constructRelation(relation, _entity)
|
||||||
);
|
);
|
||||||
|
|
||||||
const indices = transformObject(this.config.indices ?? {}, (index, name) => {
|
const indices = transformObject(this.config.indices ?? {}, (index, name) => {
|
||||||
@@ -91,7 +59,7 @@ export class AppData<DB> extends Module<typeof dataConfigSchema> {
|
|||||||
return dataConfigSchema;
|
return dataConfigSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
get em(): EntityManager<DB> {
|
get em(): EntityManager {
|
||||||
this.throwIfNotBuilt();
|
this.throwIfNotBuilt();
|
||||||
return this.ctx.em;
|
return this.ctx.em;
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-25
@@ -1,3 +1,4 @@
|
|||||||
|
import type { DB } from "core";
|
||||||
import type { EntityData, RepoQuery, RepositoryResponse } from "data";
|
import type { EntityData, RepoQuery, RepositoryResponse } from "data";
|
||||||
import { type BaseModuleApiOptions, ModuleApi, type PrimaryFieldType } from "modules";
|
import { type BaseModuleApiOptions, ModuleApi, type PrimaryFieldType } from "modules";
|
||||||
|
|
||||||
@@ -15,48 +16,60 @@ export class DataApi extends ModuleApi<DataApiOptions> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async readOne(
|
readOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||||
entity: string,
|
entity: E,
|
||||||
id: PrimaryFieldType,
|
id: PrimaryFieldType,
|
||||||
query: Partial<Omit<RepoQuery, "where" | "limit" | "offset">> = {}
|
query: Partial<Omit<RepoQuery, "where" | "limit" | "offset">> = {}
|
||||||
) {
|
) {
|
||||||
return this.get<RepositoryResponse<EntityData>>([entity, id], query);
|
return this.get<Pick<RepositoryResponse<Data>, "meta" | "data">>([entity as any, id], query);
|
||||||
}
|
}
|
||||||
|
|
||||||
async readMany(entity: string, query: Partial<RepoQuery> = {}) {
|
readMany<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||||
return this.get<Pick<RepositoryResponse, "meta" | "data">>(
|
entity: E,
|
||||||
[entity],
|
|
||||||
query ?? this.options.defaultQuery
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async readManyByReference(
|
|
||||||
entity: string,
|
|
||||||
id: PrimaryFieldType,
|
|
||||||
reference: string,
|
|
||||||
query: Partial<RepoQuery> = {}
|
query: Partial<RepoQuery> = {}
|
||||||
) {
|
) {
|
||||||
return this.get<Pick<RepositoryResponse, "meta" | "data">>(
|
return this.get<Pick<RepositoryResponse<Data[]>, "meta" | "data">>(
|
||||||
[entity, id, reference],
|
[entity as any],
|
||||||
query ?? this.options.defaultQuery
|
query ?? this.options.defaultQuery
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOne(entity: string, input: EntityData) {
|
readManyByReference<
|
||||||
return this.post<RepositoryResponse<EntityData>>([entity], input);
|
E extends keyof DB | string,
|
||||||
|
R extends keyof DB | string,
|
||||||
|
Data = R extends keyof DB ? DB[R] : EntityData
|
||||||
|
>(entity: E, id: PrimaryFieldType, reference: R, query: Partial<RepoQuery> = {}) {
|
||||||
|
return this.get<Pick<RepositoryResponse<Data[]>, "meta" | "data">>(
|
||||||
|
[entity as any, id, reference],
|
||||||
|
query ?? this.options.defaultQuery
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateOne(entity: string, id: PrimaryFieldType, input: EntityData) {
|
createOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||||
return this.patch<RepositoryResponse<EntityData>>([entity, id], input);
|
entity: E,
|
||||||
|
input: Omit<Data, "id">
|
||||||
|
) {
|
||||||
|
return this.post<RepositoryResponse<Data>>([entity as any], input);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteOne(entity: string, id: PrimaryFieldType) {
|
updateOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||||
return this.delete<RepositoryResponse<EntityData>>([entity, id]);
|
entity: E,
|
||||||
|
id: PrimaryFieldType,
|
||||||
|
input: Partial<Omit<Data, "id">>
|
||||||
|
) {
|
||||||
|
return this.patch<RepositoryResponse<Data>>([entity as any, id], input);
|
||||||
}
|
}
|
||||||
|
|
||||||
async count(entity: string, where: RepoQuery["where"] = {}) {
|
deleteOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||||
return this.post<RepositoryResponse<{ entity: string; count: number }>>(
|
entity: E,
|
||||||
[entity, "fn", "count"],
|
id: PrimaryFieldType
|
||||||
|
) {
|
||||||
|
return this.delete<RepositoryResponse<Data>>([entity as any, id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
count<E extends keyof DB | string>(entity: E, where: RepoQuery["where"] = {}) {
|
||||||
|
return this.post<RepositoryResponse<{ entity: E; count: number }>>(
|
||||||
|
[entity as any, "fn", "count"],
|
||||||
where
|
where
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { type ClassController, isDebug, tbValidator as tb } from "core";
|
import { type ClassController, isDebug, tbValidator as tb } from "core";
|
||||||
import { Type, objectCleanEmpty, objectTransform } from "core/utils";
|
import { StringEnum, Type, objectCleanEmpty, objectTransform } from "core/utils";
|
||||||
import {
|
import {
|
||||||
DataPermissions,
|
DataPermissions,
|
||||||
type EntityData,
|
type EntityData,
|
||||||
@@ -165,13 +165,12 @@ export class DataController implements ClassController {
|
|||||||
// read entity schema
|
// read entity schema
|
||||||
.get("/schema.json", async (c) => {
|
.get("/schema.json", async (c) => {
|
||||||
this.guard.throwUnlessGranted(DataPermissions.entityRead);
|
this.guard.throwUnlessGranted(DataPermissions.entityRead);
|
||||||
const url = new URL(c.req.url);
|
const $id = `${this.config.basepath}/schema.json`;
|
||||||
const $id = `${url.origin}${this.config.basepath}/schema.json`;
|
|
||||||
const schemas = Object.fromEntries(
|
const schemas = Object.fromEntries(
|
||||||
this.em.entities.map((e) => [
|
this.em.entities.map((e) => [
|
||||||
e.name,
|
e.name,
|
||||||
{
|
{
|
||||||
$ref: `schemas/${e.name}`
|
$ref: `${this.config.basepath}/schemas/${e.name}`
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
@@ -183,22 +182,28 @@ export class DataController implements ClassController {
|
|||||||
})
|
})
|
||||||
// read schema
|
// read schema
|
||||||
.get(
|
.get(
|
||||||
"/schemas/:entity",
|
"/schemas/:entity/:context?",
|
||||||
tb("param", Type.Object({ entity: Type.String() })),
|
tb(
|
||||||
|
"param",
|
||||||
|
Type.Object({
|
||||||
|
entity: Type.String(),
|
||||||
|
context: Type.Optional(StringEnum(["create", "update"]))
|
||||||
|
})
|
||||||
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
this.guard.throwUnlessGranted(DataPermissions.entityRead);
|
this.guard.throwUnlessGranted(DataPermissions.entityRead);
|
||||||
|
|
||||||
//console.log("request", c.req.raw);
|
//console.log("request", c.req.raw);
|
||||||
const { entity } = c.req.param();
|
const { entity, context } = c.req.param();
|
||||||
if (!this.entityExists(entity)) {
|
if (!this.entityExists(entity)) {
|
||||||
console.log("not found", entity, definedEntities);
|
console.log("not found", entity, definedEntities);
|
||||||
return c.notFound();
|
return c.notFound();
|
||||||
}
|
}
|
||||||
const _entity = this.em.entity(entity);
|
const _entity = this.em.entity(entity);
|
||||||
const schema = _entity.toSchema();
|
const schema = _entity.toSchema({ context } as any);
|
||||||
const url = new URL(c.req.url);
|
const url = new URL(c.req.url);
|
||||||
const base = `${url.origin}${this.config.basepath}`;
|
const base = `${url.origin}${this.config.basepath}`;
|
||||||
const $id = `${base}/schemas/${entity}`;
|
const $id = `${this.config.basepath}/schemas/${entity}`;
|
||||||
return c.json({
|
return c.json({
|
||||||
$schema: `${base}/schema.json`,
|
$schema: `${base}/schema.json`,
|
||||||
$id,
|
$id,
|
||||||
@@ -369,9 +374,9 @@ export class DataController implements ClassController {
|
|||||||
return c.notFound();
|
return c.notFound();
|
||||||
}
|
}
|
||||||
const where = c.req.valid("json") as RepoQuery["where"];
|
const where = c.req.valid("json") as RepoQuery["where"];
|
||||||
console.log("where", where);
|
//console.log("where", where);
|
||||||
|
|
||||||
const result = await this.em.mutator(entity).deleteMany(where);
|
const result = await this.em.mutator(entity).deleteWhere(where);
|
||||||
|
|
||||||
return c.json(this.mutatorResult(result));
|
return c.json(this.mutatorResult(result));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,16 +41,18 @@ export type DbFunctions = {
|
|||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export abstract class Connection {
|
const CONN_SYMBOL = Symbol.for("bknd:connection");
|
||||||
cls = "bknd:connection";
|
|
||||||
kysely: Kysely<any>;
|
export abstract class Connection<DB = any> {
|
||||||
|
kysely: Kysely<DB>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
kysely: Kysely<any>,
|
kysely: Kysely<DB>,
|
||||||
public fn: Partial<DbFunctions> = {},
|
public fn: Partial<DbFunctions> = {},
|
||||||
protected plugins: KyselyPlugin[] = []
|
protected plugins: KyselyPlugin[] = []
|
||||||
) {
|
) {
|
||||||
this.kysely = kysely;
|
this.kysely = kysely;
|
||||||
|
this[CONN_SYMBOL] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,8 +60,9 @@ export abstract class Connection {
|
|||||||
* coming from different places
|
* coming from different places
|
||||||
* @param conn
|
* @param conn
|
||||||
*/
|
*/
|
||||||
static isConnection(conn: any): conn is Connection {
|
static isConnection(conn: unknown): conn is Connection {
|
||||||
return conn?.cls === "bknd:connection";
|
if (!conn) return false;
|
||||||
|
return conn[CONN_SYMBOL] === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
getIntrospector(): ConnectionIntrospector {
|
getIntrospector(): ConnectionIntrospector {
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { Connection } from "./Connection";
|
||||||
|
|
||||||
|
export class DummyConnection extends Connection {
|
||||||
|
constructor() {
|
||||||
|
super(undefined as any);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type Client, type InStatement, createClient } from "@libsql/client/web";
|
import { type Client, type Config, type InStatement, createClient } from "@libsql/client";
|
||||||
import { LibsqlDialect } from "@libsql/kysely-libsql";
|
import { LibsqlDialect } from "@libsql/kysely-libsql";
|
||||||
import { type DatabaseIntrospector, Kysely, ParseJSONResultsPlugin, sql } from "kysely";
|
import { type DatabaseIntrospector, Kysely, ParseJSONResultsPlugin, sql } from "kysely";
|
||||||
import { FilterNumericKeysPlugin } from "../plugins/FilterNumericKeysPlugin";
|
import { FilterNumericKeysPlugin } from "../plugins/FilterNumericKeysPlugin";
|
||||||
@@ -8,9 +8,7 @@ import { SqliteConnection } from "./SqliteConnection";
|
|||||||
import { SqliteIntrospector } from "./SqliteIntrospector";
|
import { SqliteIntrospector } from "./SqliteIntrospector";
|
||||||
|
|
||||||
export const LIBSQL_PROTOCOLS = ["wss", "https", "libsql"] as const;
|
export const LIBSQL_PROTOCOLS = ["wss", "https", "libsql"] as const;
|
||||||
export type LibSqlCredentials = {
|
export type LibSqlCredentials = Config & {
|
||||||
url: string;
|
|
||||||
authToken?: string;
|
|
||||||
protocol?: (typeof LIBSQL_PROTOCOLS)[number];
|
protocol?: (typeof LIBSQL_PROTOCOLS)[number];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export class Entity<
|
|||||||
}
|
}
|
||||||
|
|
||||||
get label(): string {
|
get label(): string {
|
||||||
return snakeToPascalWithSpaces(this.config.name ?? this.name);
|
return this.config.name ?? snakeToPascalWithSpaces(this.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
field(name: string): Field | undefined {
|
field(name: string): Field | undefined {
|
||||||
@@ -210,20 +210,34 @@ export class Entity<
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
toSchema(clean?: boolean): object {
|
toSchema(options?: { clean: boolean; context?: "create" | "update" }): object {
|
||||||
const fields = Object.fromEntries(this.fields.map((field) => [field.name, field]));
|
let fields: Field[];
|
||||||
|
switch (options?.context) {
|
||||||
|
case "create":
|
||||||
|
case "update":
|
||||||
|
fields = this.getFillableFields(options.context);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
fields = this.getFields(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const _fields = Object.fromEntries(fields.map((field) => [field.name, field]));
|
||||||
const schema = Type.Object(
|
const schema = Type.Object(
|
||||||
transformObject(fields, (field) => ({
|
transformObject(_fields, (field) => {
|
||||||
title: field.config.label,
|
//const hidden = field.isHidden(options?.context);
|
||||||
$comment: field.config.description,
|
const fillable = field.isFillable(options?.context);
|
||||||
$field: field.type,
|
return {
|
||||||
readOnly: !field.isFillable("update") ? true : undefined,
|
title: field.config.label,
|
||||||
writeOnly: !field.isFillable("create") ? true : undefined,
|
$comment: field.config.description,
|
||||||
...field.toJsonSchema()
|
$field: field.type,
|
||||||
}))
|
readOnly: !fillable ? true : undefined,
|
||||||
|
...field.toJsonSchema()
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
{ additionalProperties: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
return clean ? JSON.parse(JSON.stringify(schema)) : schema;
|
return options?.clean ? JSON.parse(JSON.stringify(schema)) : schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { DB as DefaultDB } from "core";
|
||||||
import { EventManager } from "core/events";
|
import { EventManager } from "core/events";
|
||||||
import { sql } from "kysely";
|
import { sql } from "kysely";
|
||||||
import { Connection } from "../connection/Connection";
|
import { Connection } from "../connection/Connection";
|
||||||
@@ -14,7 +15,18 @@ import { SchemaManager } from "../schema/SchemaManager";
|
|||||||
import { Entity } from "./Entity";
|
import { Entity } from "./Entity";
|
||||||
import { type EntityData, Mutator, Repository } from "./index";
|
import { type EntityData, Mutator, Repository } from "./index";
|
||||||
|
|
||||||
export class EntityManager<DB> {
|
type EntitySchema<
|
||||||
|
TBD extends object = DefaultDB,
|
||||||
|
E extends Entity | keyof TBD | string = string
|
||||||
|
> = E extends Entity<infer Name>
|
||||||
|
? Name extends keyof TBD
|
||||||
|
? Name
|
||||||
|
: never
|
||||||
|
: E extends keyof TBD
|
||||||
|
? E
|
||||||
|
: never;
|
||||||
|
|
||||||
|
export class EntityManager<TBD extends object = DefaultDB> {
|
||||||
connection: Connection;
|
connection: Connection;
|
||||||
|
|
||||||
private _entities: Entity[] = [];
|
private _entities: Entity[] = [];
|
||||||
@@ -50,7 +62,7 @@ export class EntityManager<DB> {
|
|||||||
* Forks the EntityManager without the EventManager.
|
* Forks the EntityManager without the EventManager.
|
||||||
* This is useful when used inside an event handler.
|
* This is useful when used inside an event handler.
|
||||||
*/
|
*/
|
||||||
fork(): EntityManager<DB> {
|
fork(): EntityManager {
|
||||||
return new EntityManager(this._entities, this.connection, this._relations, this._indices);
|
return new EntityManager(this._entities, this.connection, this._relations, this._indices);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,10 +99,17 @@ export class EntityManager<DB> {
|
|||||||
this.entities.push(entity);
|
this.entities.push(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
entity(name: string): Entity {
|
entity(e: Entity | keyof TBD | string): Entity {
|
||||||
const entity = this.entities.find((e) => e.name === name);
|
let entity: Entity | undefined;
|
||||||
|
if (typeof e === "string") {
|
||||||
|
entity = this.entities.find((entity) => entity.name === e);
|
||||||
|
} else if (e instanceof Entity) {
|
||||||
|
entity = e;
|
||||||
|
}
|
||||||
|
|
||||||
if (!entity) {
|
if (!entity) {
|
||||||
throw new EntityNotDefinedException(name);
|
// @ts-ignore
|
||||||
|
throw new EntityNotDefinedException(e instanceof Entity ? e.name : e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return entity;
|
return entity;
|
||||||
@@ -162,28 +181,18 @@ export class EntityManager<DB> {
|
|||||||
return this.relations.relationReferencesOf(this.entity(entity_name));
|
return this.relations.relationReferencesOf(this.entity(entity_name));
|
||||||
}
|
}
|
||||||
|
|
||||||
repository(_entity: Entity | string) {
|
repository<E extends Entity | keyof TBD | string>(
|
||||||
const entity = _entity instanceof Entity ? _entity : this.entity(_entity);
|
entity: E
|
||||||
return new Repository(this, entity, this.emgr);
|
): Repository<TBD, EntitySchema<TBD, E>> {
|
||||||
|
return this.repo(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
repo<E extends Entity>(
|
repo<E extends Entity | keyof TBD | string>(entity: E): Repository<TBD, EntitySchema<TBD, E>> {
|
||||||
_entity: E
|
return new Repository(this, this.entity(entity), this.emgr);
|
||||||
): Repository<
|
|
||||||
DB,
|
|
||||||
E extends Entity<infer Name> ? (Name extends keyof DB ? Name : never) : never
|
|
||||||
> {
|
|
||||||
return new Repository(this, _entity, this.emgr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_repo<TB extends keyof DB>(_entity: TB): Repository<DB, TB> {
|
mutator<E extends Entity | keyof TBD | string>(entity: E): Mutator<TBD, EntitySchema<TBD, E>> {
|
||||||
const entity = this.entity(_entity as any);
|
return new Mutator(this, this.entity(entity), this.emgr);
|
||||||
return new Repository(this, entity, this.emgr);
|
|
||||||
}
|
|
||||||
|
|
||||||
mutator(_entity: Entity | string) {
|
|
||||||
const entity = _entity instanceof Entity ? _entity : this.entity(_entity);
|
|
||||||
return new Mutator(this, entity, this.emgr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addIndex(index: EntityIndex, force = false) {
|
addIndex(index: EntityIndex, force = false) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { PrimaryFieldType } from "core";
|
import type { DB as DefaultDB, PrimaryFieldType } from "core";
|
||||||
import { type EmitsEvents, EventManager } from "core/events";
|
import { type EmitsEvents, EventManager } from "core/events";
|
||||||
import type { DeleteQueryBuilder, InsertQueryBuilder, UpdateQueryBuilder } from "kysely";
|
import type { DeleteQueryBuilder, InsertQueryBuilder, UpdateQueryBuilder } from "kysely";
|
||||||
import { type TActionContext, WhereBuilder } from "..";
|
import { type TActionContext, WhereBuilder } from "..";
|
||||||
@@ -25,8 +25,14 @@ export type MutatorResponse<T = EntityData[]> = {
|
|||||||
data: T;
|
data: T;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Mutator<DB> implements EmitsEvents {
|
export class Mutator<
|
||||||
em: EntityManager<DB>;
|
TBD extends object = DefaultDB,
|
||||||
|
TB extends keyof TBD = any,
|
||||||
|
Output = TBD[TB],
|
||||||
|
Input = Omit<Output, "id">
|
||||||
|
> implements EmitsEvents
|
||||||
|
{
|
||||||
|
em: EntityManager<TBD>;
|
||||||
entity: Entity;
|
entity: Entity;
|
||||||
static readonly Events = MutatorEvents;
|
static readonly Events = MutatorEvents;
|
||||||
emgr: EventManager<typeof MutatorEvents>;
|
emgr: EventManager<typeof MutatorEvents>;
|
||||||
@@ -37,7 +43,7 @@ export class Mutator<DB> implements EmitsEvents {
|
|||||||
this.__unstable_disable_system_entity_creation = value;
|
this.__unstable_disable_system_entity_creation = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(em: EntityManager<DB>, entity: Entity, emgr?: EventManager<any>) {
|
constructor(em: EntityManager<TBD>, entity: Entity, emgr?: EventManager<any>) {
|
||||||
this.em = em;
|
this.em = em;
|
||||||
this.entity = entity;
|
this.entity = entity;
|
||||||
this.emgr = emgr ?? new EventManager(MutatorEvents);
|
this.emgr = emgr ?? new EventManager(MutatorEvents);
|
||||||
@@ -47,13 +53,13 @@ export class Mutator<DB> implements EmitsEvents {
|
|||||||
return this.em.connection.kysely;
|
return this.em.connection.kysely;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getValidatedData(data: EntityData, context: TActionContext): Promise<EntityData> {
|
async getValidatedData<Given = any>(data: Given, context: TActionContext): Promise<Given> {
|
||||||
const entity = this.entity;
|
const entity = this.entity;
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("Context must be provided for validation");
|
throw new Error("Context must be provided for validation");
|
||||||
}
|
}
|
||||||
|
|
||||||
const keys = Object.keys(data);
|
const keys = Object.keys(data as any);
|
||||||
const validatedData: EntityData = {};
|
const validatedData: EntityData = {};
|
||||||
|
|
||||||
// get relational references/keys
|
// get relational references/keys
|
||||||
@@ -95,7 +101,7 @@ export class Mutator<DB> implements EmitsEvents {
|
|||||||
throw new Error(`No data left to update "${entity.name}"`);
|
throw new Error(`No data left to update "${entity.name}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return validatedData;
|
return validatedData as Given;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async many(qb: MutatorQB): Promise<MutatorResponse> {
|
protected async many(qb: MutatorQB): Promise<MutatorResponse> {
|
||||||
@@ -120,7 +126,7 @@ export class Mutator<DB> implements EmitsEvents {
|
|||||||
return { ...response, data: data[0]! };
|
return { ...response, data: data[0]! };
|
||||||
}
|
}
|
||||||
|
|
||||||
async insertOne(data: EntityData): Promise<MutatorResponse<EntityData>> {
|
async insertOne(data: Input): Promise<MutatorResponse<Output>> {
|
||||||
const entity = this.entity;
|
const entity = this.entity;
|
||||||
if (entity.type === "system" && this.__unstable_disable_system_entity_creation) {
|
if (entity.type === "system" && this.__unstable_disable_system_entity_creation) {
|
||||||
throw new Error(`Creation of system entity "${entity.name}" is disabled`);
|
throw new Error(`Creation of system entity "${entity.name}" is disabled`);
|
||||||
@@ -154,10 +160,10 @@ export class Mutator<DB> implements EmitsEvents {
|
|||||||
|
|
||||||
await this.emgr.emit(new Mutator.Events.MutatorInsertAfter({ entity, data: res.data }));
|
await this.emgr.emit(new Mutator.Events.MutatorInsertAfter({ entity, data: res.data }));
|
||||||
|
|
||||||
return res;
|
return res as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateOne(id: PrimaryFieldType, data: EntityData): Promise<MutatorResponse<EntityData>> {
|
async updateOne(id: PrimaryFieldType, data: Partial<Input>): Promise<MutatorResponse<Output>> {
|
||||||
const entity = this.entity;
|
const entity = this.entity;
|
||||||
if (!Number.isInteger(id)) {
|
if (!Number.isInteger(id)) {
|
||||||
throw new Error("ID must be provided for update");
|
throw new Error("ID must be provided for update");
|
||||||
@@ -166,12 +172,16 @@ export class Mutator<DB> implements EmitsEvents {
|
|||||||
const validatedData = await this.getValidatedData(data, "update");
|
const validatedData = await this.getValidatedData(data, "update");
|
||||||
|
|
||||||
await this.emgr.emit(
|
await this.emgr.emit(
|
||||||
new Mutator.Events.MutatorUpdateBefore({ entity, entityId: id, data: validatedData })
|
new Mutator.Events.MutatorUpdateBefore({
|
||||||
|
entity,
|
||||||
|
entityId: id,
|
||||||
|
data: validatedData as any
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const query = this.conn
|
const query = this.conn
|
||||||
.updateTable(entity.name)
|
.updateTable(entity.name)
|
||||||
.set(validatedData)
|
.set(validatedData as any)
|
||||||
.where(entity.id().name, "=", id)
|
.where(entity.id().name, "=", id)
|
||||||
.returning(entity.getSelect());
|
.returning(entity.getSelect());
|
||||||
|
|
||||||
@@ -181,10 +191,10 @@ export class Mutator<DB> implements EmitsEvents {
|
|||||||
new Mutator.Events.MutatorUpdateAfter({ entity, entityId: id, data: res.data })
|
new Mutator.Events.MutatorUpdateAfter({ entity, entityId: id, data: res.data })
|
||||||
);
|
);
|
||||||
|
|
||||||
return res;
|
return res as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteOne(id: PrimaryFieldType): Promise<MutatorResponse<EntityData>> {
|
async deleteOne(id: PrimaryFieldType): Promise<MutatorResponse<Output>> {
|
||||||
const entity = this.entity;
|
const entity = this.entity;
|
||||||
if (!Number.isInteger(id)) {
|
if (!Number.isInteger(id)) {
|
||||||
throw new Error("ID must be provided for deletion");
|
throw new Error("ID must be provided for deletion");
|
||||||
@@ -203,7 +213,7 @@ export class Mutator<DB> implements EmitsEvents {
|
|||||||
new Mutator.Events.MutatorDeleteAfter({ entity, entityId: id, data: res.data })
|
new Mutator.Events.MutatorDeleteAfter({ entity, entityId: id, data: res.data })
|
||||||
);
|
);
|
||||||
|
|
||||||
return res;
|
return res as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getValidOptions(options?: Partial<RepoQuery>): Partial<RepoQuery> {
|
private getValidOptions(options?: Partial<RepoQuery>): Partial<RepoQuery> {
|
||||||
@@ -250,21 +260,62 @@ export class Mutator<DB> implements EmitsEvents {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @todo: decide whether entries should be deleted all at once or one by one (for events)
|
// @todo: decide whether entries should be deleted all at once or one by one (for events)
|
||||||
async deleteMany(where?: RepoQuery["where"]): Promise<MutatorResponse<EntityData>> {
|
async deleteWhere(where?: RepoQuery["where"]): Promise<MutatorResponse<Output[]>> {
|
||||||
const entity = this.entity;
|
const entity = this.entity;
|
||||||
|
|
||||||
const qb = this.appendWhere(this.conn.deleteFrom(entity.name), where).returning(
|
const qb = this.appendWhere(this.conn.deleteFrom(entity.name), where).returning(
|
||||||
entity.getSelect()
|
entity.getSelect()
|
||||||
);
|
);
|
||||||
|
|
||||||
//await this.emgr.emit(new Mutator.Events.MutatorDeleteBefore({ entity, entityId: id }));
|
return (await this.many(qb)) as any;
|
||||||
|
}
|
||||||
|
|
||||||
const res = await this.many(qb);
|
async updateWhere(
|
||||||
|
data: Partial<Input>,
|
||||||
|
where?: RepoQuery["where"]
|
||||||
|
): Promise<MutatorResponse<Output[]>> {
|
||||||
|
const entity = this.entity;
|
||||||
|
const validatedData = await this.getValidatedData(data, "update");
|
||||||
|
|
||||||
/*await this.emgr.emit(
|
const query = this.appendWhere(this.conn.updateTable(entity.name), where)
|
||||||
new Mutator.Events.MutatorDeleteAfter({ entity, entityId: id, data: res.data })
|
.set(validatedData as any)
|
||||||
);*/
|
.returning(entity.getSelect());
|
||||||
|
|
||||||
return res;
|
return (await this.many(query)) as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
async insertMany(data: Input[]): Promise<MutatorResponse<Output[]>> {
|
||||||
|
const entity = this.entity;
|
||||||
|
if (entity.type === "system" && this.__unstable_disable_system_entity_creation) {
|
||||||
|
throw new Error(`Creation of system entity "${entity.name}" is disabled`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const validated: any[] = [];
|
||||||
|
for (const row of data) {
|
||||||
|
const validatedData = {
|
||||||
|
...entity.getDefaultObject(),
|
||||||
|
...(await this.getValidatedData(row, "create"))
|
||||||
|
};
|
||||||
|
|
||||||
|
// check if required fields are present
|
||||||
|
const required = entity.getRequiredFields();
|
||||||
|
for (const field of required) {
|
||||||
|
if (
|
||||||
|
typeof validatedData[field.name] === "undefined" ||
|
||||||
|
validatedData[field.name] === null
|
||||||
|
) {
|
||||||
|
throw new Error(`Field "${field.name}" is required`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validated.push(validatedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = this.conn
|
||||||
|
.insertInto(entity.name)
|
||||||
|
.values(validated)
|
||||||
|
.returning(entity.getSelect());
|
||||||
|
|
||||||
|
return (await this.many(query)) as any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { PrimaryFieldType } from "core";
|
import type { DB as DefaultDB, PrimaryFieldType } from "core";
|
||||||
import { type EmitsEvents, EventManager } from "core/events";
|
import { type EmitsEvents, EventManager } from "core/events";
|
||||||
import { type SelectQueryBuilder, sql } from "kysely";
|
import { type SelectQueryBuilder, sql } from "kysely";
|
||||||
import { cloneDeep } from "lodash-es";
|
import { cloneDeep } from "lodash-es";
|
||||||
@@ -43,13 +43,15 @@ export type RepositoryExistsResponse = RepositoryRawResponse & {
|
|||||||
exists: boolean;
|
exists: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Repository<DB = any, TB extends keyof DB = any> implements EmitsEvents {
|
export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = any>
|
||||||
em: EntityManager<DB>;
|
implements EmitsEvents
|
||||||
|
{
|
||||||
|
em: EntityManager<TBD>;
|
||||||
entity: Entity;
|
entity: Entity;
|
||||||
static readonly Events = RepositoryEvents;
|
static readonly Events = RepositoryEvents;
|
||||||
emgr: EventManager<typeof Repository.Events>;
|
emgr: EventManager<typeof Repository.Events>;
|
||||||
|
|
||||||
constructor(em: EntityManager<DB>, entity: Entity, emgr?: EventManager<any>) {
|
constructor(em: EntityManager<TBD>, entity: Entity, emgr?: EventManager<any>) {
|
||||||
this.em = em;
|
this.em = em;
|
||||||
this.entity = entity;
|
this.entity = entity;
|
||||||
this.emgr = emgr ?? new EventManager(MutatorEvents);
|
this.emgr = emgr ?? new EventManager(MutatorEvents);
|
||||||
@@ -162,8 +164,7 @@ export class Repository<DB = any, TB extends keyof DB = any> implements EmitsEve
|
|||||||
protected async performQuery(qb: RepositoryQB): Promise<RepositoryResponse> {
|
protected async performQuery(qb: RepositoryQB): Promise<RepositoryResponse> {
|
||||||
const entity = this.entity;
|
const entity = this.entity;
|
||||||
const compiled = qb.compile();
|
const compiled = qb.compile();
|
||||||
/*const { sql, parameters } = qb.compile();
|
//console.log("performQuery", compiled.sql, compiled.parameters);
|
||||||
console.log("many", sql, parameters);*/
|
|
||||||
|
|
||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
const selector = (as = "count") => this.conn.fn.countAll<number>().as(as);
|
const selector = (as = "count") => this.conn.fn.countAll<number>().as(as);
|
||||||
@@ -202,7 +203,10 @@ export class Repository<DB = any, TB extends keyof DB = any> implements EmitsEve
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("many error", e, compiled);
|
if (e instanceof Error) {
|
||||||
|
console.error("[ERROR] Repository.performQuery", e.message);
|
||||||
|
}
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -263,13 +267,14 @@ export class Repository<DB = any, TB extends keyof DB = any> implements EmitsEve
|
|||||||
qb = qb.orderBy(aliased(options.sort.by), options.sort.dir);
|
qb = qb.orderBy(aliased(options.sort.by), options.sort.dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//console.log("options", { _options, options, exclude_options });
|
||||||
return { qb, options };
|
return { qb, options };
|
||||||
}
|
}
|
||||||
|
|
||||||
async findId(
|
async findId(
|
||||||
id: PrimaryFieldType,
|
id: PrimaryFieldType,
|
||||||
_options?: Partial<Omit<RepoQuery, "where" | "limit" | "offset">>
|
_options?: Partial<Omit<RepoQuery, "where" | "limit" | "offset">>
|
||||||
): Promise<RepositoryResponse<DB[TB]>> {
|
): Promise<RepositoryResponse<TBD[TB] | undefined>> {
|
||||||
const { qb, options } = this.buildQuery(
|
const { qb, options } = this.buildQuery(
|
||||||
{
|
{
|
||||||
..._options,
|
..._options,
|
||||||
@@ -285,20 +290,17 @@ export class Repository<DB = any, TB extends keyof DB = any> implements EmitsEve
|
|||||||
async findOne(
|
async findOne(
|
||||||
where: RepoQuery["where"],
|
where: RepoQuery["where"],
|
||||||
_options?: Partial<Omit<RepoQuery, "where" | "limit" | "offset">>
|
_options?: Partial<Omit<RepoQuery, "where" | "limit" | "offset">>
|
||||||
): Promise<RepositoryResponse<DB[TB] | undefined>> {
|
): Promise<RepositoryResponse<TBD[TB] | undefined>> {
|
||||||
const { qb, options } = this.buildQuery(
|
const { qb, options } = this.buildQuery({
|
||||||
{
|
..._options,
|
||||||
..._options,
|
where,
|
||||||
where,
|
limit: 1
|
||||||
limit: 1
|
});
|
||||||
},
|
|
||||||
["offset", "sort"]
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.single(qb, options) as any;
|
return this.single(qb, options) as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findMany(_options?: Partial<RepoQuery>): Promise<RepositoryResponse<DB[TB][]>> {
|
async findMany(_options?: Partial<RepoQuery>): Promise<RepositoryResponse<TBD[TB][]>> {
|
||||||
const { qb, options } = this.buildQuery(_options);
|
const { qb, options } = this.buildQuery(_options);
|
||||||
//console.log("findMany:options", options);
|
//console.log("findMany:options", options);
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import type {
|
|||||||
SelectQueryBuilder,
|
SelectQueryBuilder,
|
||||||
UpdateQueryBuilder
|
UpdateQueryBuilder
|
||||||
} from "kysely";
|
} from "kysely";
|
||||||
import type { RepositoryQB } from "./Repository";
|
|
||||||
|
|
||||||
type Builder = ExpressionBuilder<any, any>;
|
type Builder = ExpressionBuilder<any, any>;
|
||||||
type Wrapper = ExpressionWrapper<any, any, any>;
|
type Wrapper = ExpressionWrapper<any, any, any>;
|
||||||
|
|||||||
@@ -236,8 +236,8 @@ export abstract class Field<
|
|||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
//name: this.name,
|
// @todo: current workaround because of fixed string type
|
||||||
type: this.type,
|
type: this.type as any,
|
||||||
config: this.config
|
config: this.config
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export class JsonSchemaField<
|
|||||||
|
|
||||||
if (parentValid) {
|
if (parentValid) {
|
||||||
// already checked in parent
|
// already checked in parent
|
||||||
if (!value || typeof value !== "object") {
|
if (!this.isRequired() && (!value || typeof value !== "object")) {
|
||||||
//console.log("jsonschema:valid: not checking", this.name, value, context);
|
//console.log("jsonschema:valid: not checking", this.name, value, context);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -65,6 +65,7 @@ export class JsonSchemaField<
|
|||||||
} else {
|
} else {
|
||||||
//console.log("jsonschema:invalid", this.name, value, context);
|
//console.log("jsonschema:invalid", this.name, value, context);
|
||||||
}
|
}
|
||||||
|
//console.log("jsonschema:invalid:fromParent", this.name, value, context);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -110,9 +111,13 @@ export class JsonSchemaField<
|
|||||||
): Promise<string | undefined> {
|
): Promise<string | undefined> {
|
||||||
const value = await super.transformPersist(_value, em, context);
|
const value = await super.transformPersist(_value, em, context);
|
||||||
if (this.nullish(value)) return value;
|
if (this.nullish(value)) return value;
|
||||||
|
//console.log("jsonschema:transformPersist", this.name, _value, context);
|
||||||
|
|
||||||
if (!this.isValid(value)) {
|
if (!this.isValid(value)) {
|
||||||
|
//console.error("jsonschema:transformPersist:invalid", this.name, value);
|
||||||
throw new TransformPersistFailedException(this.name, value);
|
throw new TransformPersistFailedException(this.name, value);
|
||||||
|
} else {
|
||||||
|
//console.log("jsonschema:transformPersist:valid", this.name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!value || typeof value !== "object") return this.getDefault();
|
if (!value || typeof value !== "object") return this.getDefault();
|
||||||
|
|||||||
@@ -104,6 +104,12 @@ export class TextField<Required extends true | false = false> extends Field<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.config.pattern && value && !new RegExp(this.config.pattern).test(value)) {
|
||||||
|
throw new TransformPersistFailedException(
|
||||||
|
`Field "${this.name}" must match the pattern ${this.config.pattern}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export function getChangeSet(
|
|||||||
data: EntityData,
|
data: EntityData,
|
||||||
fields: Field[]
|
fields: Field[]
|
||||||
): EntityData {
|
): EntityData {
|
||||||
|
//console.log("getChangeSet", formData, data);
|
||||||
return transform(
|
return transform(
|
||||||
formData,
|
formData,
|
||||||
(acc, _value, key) => {
|
(acc, _value, key) => {
|
||||||
@@ -26,11 +27,12 @@ export function getChangeSet(
|
|||||||
if (!field || field.isVirtual()) return;
|
if (!field || field.isVirtual()) return;
|
||||||
const value = _value === "" ? null : _value;
|
const value = _value === "" ? null : _value;
|
||||||
|
|
||||||
const newValue = field.getValue(value, "submit");
|
// normalize to null if undefined
|
||||||
|
const newValue = field.getValue(value, "submit") || null;
|
||||||
// @todo: add typing for "action"
|
// @todo: add typing for "action"
|
||||||
if (action === "create" || newValue !== data[key]) {
|
if (action === "create" || newValue !== data[key]) {
|
||||||
acc[key] = newValue;
|
acc[key] = newValue;
|
||||||
console.log("changed", {
|
/*console.log("changed", {
|
||||||
key,
|
key,
|
||||||
value,
|
value,
|
||||||
valueType: typeof value,
|
valueType: typeof value,
|
||||||
@@ -38,7 +40,7 @@ export function getChangeSet(
|
|||||||
newValue,
|
newValue,
|
||||||
new: value,
|
new: value,
|
||||||
sent: acc[key]
|
sent: acc[key]
|
||||||
});
|
});*/
|
||||||
} else {
|
} else {
|
||||||
//console.log("no change", key, value, data[key]);
|
//console.log("no change", key, value, data[key]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export * from "./fields";
|
|||||||
export * from "./entities";
|
export * from "./entities";
|
||||||
export * from "./relations";
|
export * from "./relations";
|
||||||
export * from "./schema/SchemaManager";
|
export * from "./schema/SchemaManager";
|
||||||
|
export * from "./prototype";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
type RepoQuery,
|
type RepoQuery,
|
||||||
@@ -12,13 +13,13 @@ export {
|
|||||||
whereSchema
|
whereSchema
|
||||||
} from "./server/data-query-impl";
|
} from "./server/data-query-impl";
|
||||||
|
|
||||||
export { whereRepoSchema as deprecated__whereRepoSchema } from "./server/query";
|
|
||||||
|
|
||||||
export { Connection } from "./connection/Connection";
|
export { Connection } from "./connection/Connection";
|
||||||
export { LibsqlConnection, type LibSqlCredentials } from "./connection/LibsqlConnection";
|
export { LibsqlConnection, type LibSqlCredentials } from "./connection/LibsqlConnection";
|
||||||
export { SqliteConnection } from "./connection/SqliteConnection";
|
export { SqliteConnection } from "./connection/SqliteConnection";
|
||||||
export { SqliteLocalConnection } from "./connection/SqliteLocalConnection";
|
export { SqliteLocalConnection } from "./connection/SqliteLocalConnection";
|
||||||
|
|
||||||
|
export { constructEntity, constructRelation } from "./schema/constructor";
|
||||||
|
|
||||||
export const DatabaseEvents = {
|
export const DatabaseEvents = {
|
||||||
...MutatorEvents,
|
...MutatorEvents,
|
||||||
...RepositoryEvents
|
...RepositoryEvents
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
import { DummyConnection } from "data/connection/DummyConnection";
|
||||||
|
import { EntityManager } from "data/entities/EntityManager";
|
||||||
|
import type { Generated } from "kysely";
|
||||||
|
import { MediaField, type MediaFieldConfig, type MediaItem } from "media/MediaField";
|
||||||
|
import type { ModuleConfigs } from "modules";
|
||||||
import {
|
import {
|
||||||
BooleanField,
|
BooleanField,
|
||||||
type BooleanFieldConfig,
|
type BooleanFieldConfig,
|
||||||
@@ -5,6 +10,8 @@ import {
|
|||||||
type DateFieldConfig,
|
type DateFieldConfig,
|
||||||
Entity,
|
Entity,
|
||||||
type EntityConfig,
|
type EntityConfig,
|
||||||
|
EntityIndex,
|
||||||
|
type EntityRelation,
|
||||||
EnumField,
|
EnumField,
|
||||||
type EnumFieldConfig,
|
type EnumFieldConfig,
|
||||||
type Field,
|
type Field,
|
||||||
@@ -25,15 +32,14 @@ import {
|
|||||||
type TEntityType,
|
type TEntityType,
|
||||||
TextField,
|
TextField,
|
||||||
type TextFieldConfig
|
type TextFieldConfig
|
||||||
} from "data";
|
} from "../index";
|
||||||
import type { Generated } from "kysely";
|
|
||||||
import { MediaField, type MediaFieldConfig, type MediaItem } from "media/MediaField";
|
|
||||||
|
|
||||||
type Options<Config = any> = {
|
type Options<Config = any> = {
|
||||||
entity: { name: string; fields: Record<string, Field<any, any, any>> };
|
entity: { name: string; fields: Record<string, Field<any, any, any>> };
|
||||||
field_name: string;
|
field_name: string;
|
||||||
config: Config;
|
config: Config;
|
||||||
is_required: boolean;
|
is_required: boolean;
|
||||||
|
another?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FieldMap = {
|
const FieldMap = {
|
||||||
@@ -239,7 +245,89 @@ export function relation<Local extends Entity>(local: Local) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
type InferEntityFields<T> = T extends Entity<infer _N, infer Fields>
|
export function index<E extends Entity>(entity: E) {
|
||||||
|
return {
|
||||||
|
on: (fields: (keyof InsertSchema<E>)[], unique?: boolean) => {
|
||||||
|
const _fields = fields.map((f) => {
|
||||||
|
const field = entity.field(f as any);
|
||||||
|
if (!field) {
|
||||||
|
throw new Error(`Field "${String(f)}" not found on entity "${entity.name}"`);
|
||||||
|
}
|
||||||
|
return field;
|
||||||
|
});
|
||||||
|
return new EntityIndex(entity, _fields, unique);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class EntityManagerPrototype<Entities extends Record<string, Entity>> extends EntityManager<
|
||||||
|
Schema<Entities>
|
||||||
|
> {
|
||||||
|
constructor(
|
||||||
|
public __entities: Entities,
|
||||||
|
relations: EntityRelation[] = [],
|
||||||
|
indices: EntityIndex[] = []
|
||||||
|
) {
|
||||||
|
super(Object.values(__entities), new DummyConnection(), relations, indices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Chained<Fn extends (...args: any[]) => any, Rt = ReturnType<Fn>> = <E extends Entity>(
|
||||||
|
e: E
|
||||||
|
) => {
|
||||||
|
[K in keyof Rt]: Rt[K] extends (...args: any[]) => any
|
||||||
|
? (...args: Parameters<Rt[K]>) => Rt
|
||||||
|
: never;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function em<Entities extends Record<string, Entity>>(
|
||||||
|
entities: Entities,
|
||||||
|
schema?: (
|
||||||
|
fns: { relation: Chained<typeof relation>; index: Chained<typeof index> },
|
||||||
|
entities: Entities
|
||||||
|
) => void
|
||||||
|
) {
|
||||||
|
const relations: EntityRelation[] = [];
|
||||||
|
const indices: EntityIndex[] = [];
|
||||||
|
|
||||||
|
const relationProxy = (e: Entity) => {
|
||||||
|
return new Proxy(relation(e), {
|
||||||
|
get(target, prop) {
|
||||||
|
return (...args: any[]) => {
|
||||||
|
relations.push(target[prop](...args));
|
||||||
|
return relationProxy(e);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}) as any;
|
||||||
|
};
|
||||||
|
|
||||||
|
const indexProxy = (e: Entity) => {
|
||||||
|
return new Proxy(index(e), {
|
||||||
|
get(target, prop) {
|
||||||
|
return (...args: any[]) => {
|
||||||
|
indices.push(target[prop](...args));
|
||||||
|
return indexProxy(e);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}) as any;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (schema) {
|
||||||
|
schema({ relation: relationProxy, index: indexProxy }, entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
const e = new EntityManagerPrototype(entities, relations, indices);
|
||||||
|
return {
|
||||||
|
DB: e.__entities as unknown as Schemas<Entities>,
|
||||||
|
entities: e.__entities,
|
||||||
|
relations,
|
||||||
|
indices,
|
||||||
|
toJSON: () =>
|
||||||
|
e.toJSON() as unknown as Pick<ModuleConfigs["data"], "entities" | "relations" | "indices">
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InferEntityFields<T> = T extends Entity<infer _N, infer Fields>
|
||||||
? {
|
? {
|
||||||
[K in keyof Fields]: Fields[K] extends { _type: infer Type; _required: infer Required }
|
[K in keyof Fields]: Fields[K] extends { _type: infer Type; _required: infer Required }
|
||||||
? Required extends true
|
? Required extends true
|
||||||
@@ -284,12 +372,16 @@ type OptionalUndefined<
|
|||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
type InferField<Field> = Field extends { _type: infer Type; _required: infer Required }
|
export type InferField<Field> = Field extends { _type: infer Type; _required: infer Required }
|
||||||
? Required extends true
|
? Required extends true
|
||||||
? Type
|
? Type
|
||||||
: Type | undefined
|
: Type | undefined
|
||||||
: never;
|
: never;
|
||||||
|
|
||||||
|
export type Schemas<T extends Record<string, Entity>> = {
|
||||||
|
[K in keyof T]: Schema<T[K]>;
|
||||||
|
};
|
||||||
|
|
||||||
export type InsertSchema<T> = Simplify<OptionalUndefined<InferEntityFields<T>>>;
|
export type InsertSchema<T> = Simplify<OptionalUndefined<InferEntityFields<T>>>;
|
||||||
export type Schema<T> = { id: Generated<number> } & InsertSchema<T>;
|
export type Schema<T> = Simplify<{ id: Generated<number> } & InsertSchema<T>>;
|
||||||
export type FieldSchema<T> = Simplify<OptionalUndefined<InferFields<T>>>;
|
export type FieldSchema<T> = Simplify<OptionalUndefined<InferFields<T>>>;
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { transformObject } from "core/utils";
|
||||||
|
import { Entity, type Field } from "data";
|
||||||
|
import { FIELDS, RELATIONS, type TAppDataEntity, type TAppDataRelation } from "data/data-schema";
|
||||||
|
|
||||||
|
export function constructEntity(name: string, entityConfig: TAppDataEntity) {
|
||||||
|
const fields = transformObject(entityConfig.fields ?? {}, (fieldConfig, name) => {
|
||||||
|
const { type } = fieldConfig;
|
||||||
|
if (!(type in FIELDS)) {
|
||||||
|
throw new Error(`Field type "${type}" not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { field } = FIELDS[type as any];
|
||||||
|
const returnal = new field(name, fieldConfig.config) as Field;
|
||||||
|
return returnal;
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Entity(
|
||||||
|
name,
|
||||||
|
Object.values(fields),
|
||||||
|
entityConfig.config as any,
|
||||||
|
entityConfig.type as any
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function constructRelation(
|
||||||
|
relationConfig: TAppDataRelation,
|
||||||
|
resolver: (name: Entity | string) => Entity
|
||||||
|
) {
|
||||||
|
return new RELATIONS[relationConfig.type].cls(
|
||||||
|
resolver(relationConfig.source),
|
||||||
|
resolver(relationConfig.target),
|
||||||
|
relationConfig.config
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -72,6 +72,6 @@ export const querySchema = Type.Object(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export type RepoQueryIn = Simplify<Static<typeof querySchema>>;
|
export type RepoQueryIn = Static<typeof querySchema>;
|
||||||
export type RepoQuery = Required<StaticDecode<typeof querySchema>>;
|
export type RepoQuery = Required<StaticDecode<typeof querySchema>>;
|
||||||
export const defaultQuerySchema = Value.Default(querySchema, {}) as RepoQuery;
|
export const defaultQuerySchema = Value.Default(querySchema, {}) as RepoQuery;
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const date = z.union([z.date(), z.string()]);
|
|
||||||
const numeric = z.union([z.number(), date]);
|
|
||||||
const boolean = z.union([z.boolean(), z.literal(1), z.literal(0)]);
|
|
||||||
const value = z.union([z.string(), boolean, numeric]);
|
|
||||||
|
|
||||||
const expressionCond = z.union([
|
|
||||||
z.object({ $eq: value }).strict(),
|
|
||||||
z.object({ $ne: value }).strict(),
|
|
||||||
z.object({ $isnull: boolean }).strict(),
|
|
||||||
z.object({ $notnull: boolean }).strict(),
|
|
||||||
z.object({ $in: z.array(value) }).strict(),
|
|
||||||
z.object({ $notin: z.array(value) }).strict(),
|
|
||||||
z.object({ $gt: numeric }).strict(),
|
|
||||||
z.object({ $gte: numeric }).strict(),
|
|
||||||
z.object({ $lt: numeric }).strict(),
|
|
||||||
z.object({ $lte: numeric }).strict(),
|
|
||||||
z.object({ $between: z.array(numeric).min(2).max(2) }).strict()
|
|
||||||
] as const);
|
|
||||||
|
|
||||||
// prettier-ignore
|
|
||||||
const nonOperandString = z
|
|
||||||
.string()
|
|
||||||
.regex(/^(?!\$).*/)
|
|
||||||
.min(1);
|
|
||||||
|
|
||||||
// {name: 'Michael'}
|
|
||||||
const literalCond = z.record(nonOperandString, value);
|
|
||||||
|
|
||||||
// { status: { $eq: 1 } }
|
|
||||||
const literalExpressionCond = z.record(nonOperandString, value.or(expressionCond));
|
|
||||||
|
|
||||||
const operandCond = z
|
|
||||||
.object({
|
|
||||||
$and: z.array(literalCond.or(expressionCond).or(literalExpressionCond)).optional(),
|
|
||||||
$or: z.array(literalCond.or(expressionCond).or(literalExpressionCond)).optional()
|
|
||||||
})
|
|
||||||
.strict();
|
|
||||||
|
|
||||||
const literalSchema = literalCond.or(literalExpressionCond);
|
|
||||||
export type LiteralSchemaIn = z.input<typeof literalSchema>;
|
|
||||||
export type LiteralSchema = z.output<typeof literalSchema>;
|
|
||||||
|
|
||||||
export const filterSchema = literalSchema.or(operandCond);
|
|
||||||
export type FilterSchemaIn = z.input<typeof filterSchema>;
|
|
||||||
export type FilterSchema = z.output<typeof filterSchema>;
|
|
||||||
|
|
||||||
const stringArray = z
|
|
||||||
.union([
|
|
||||||
z.string().transform((v) => {
|
|
||||||
if (v.includes(",")) return v.split(",");
|
|
||||||
return v;
|
|
||||||
}),
|
|
||||||
z.array(z.string())
|
|
||||||
])
|
|
||||||
.default([])
|
|
||||||
.transform((v) => (Array.isArray(v) ? v : [v]));
|
|
||||||
|
|
||||||
export const whereRepoSchema = z
|
|
||||||
.preprocess((v: unknown) => {
|
|
||||||
try {
|
|
||||||
return JSON.parse(v as string);
|
|
||||||
} catch {
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
}, filterSchema)
|
|
||||||
.default({});
|
|
||||||
|
|
||||||
const repoQuerySchema = z.object({
|
|
||||||
limit: z.coerce.number().default(10),
|
|
||||||
offset: z.coerce.number().default(0),
|
|
||||||
sort: z
|
|
||||||
.preprocess(
|
|
||||||
(v: unknown) => {
|
|
||||||
try {
|
|
||||||
return JSON.parse(v as string);
|
|
||||||
} catch {
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
z.union([
|
|
||||||
z.string().transform((v) => {
|
|
||||||
if (v.includes(":")) {
|
|
||||||
let [field, dir] = v.split(":") as [string, string];
|
|
||||||
if (!["asc", "desc"].includes(dir)) dir = "asc";
|
|
||||||
return { by: field, dir } as { by: string; dir: "asc" | "desc" };
|
|
||||||
} else {
|
|
||||||
return { by: v, dir: "asc" } as { by: string; dir: "asc" | "desc" };
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
z.object({
|
|
||||||
by: z.string(),
|
|
||||||
dir: z.enum(["asc", "desc"])
|
|
||||||
})
|
|
||||||
])
|
|
||||||
)
|
|
||||||
.default({ by: "id", dir: "asc" }),
|
|
||||||
select: stringArray,
|
|
||||||
with: stringArray,
|
|
||||||
join: stringArray,
|
|
||||||
debug: z
|
|
||||||
.preprocess((v) => {
|
|
||||||
if (["0", "false"].includes(String(v))) return false;
|
|
||||||
return Boolean(v);
|
|
||||||
}, z.boolean())
|
|
||||||
.default(false), //z.coerce.boolean().catch(false),
|
|
||||||
where: whereRepoSchema
|
|
||||||
});
|
|
||||||
|
|
||||||
type RepoQueryIn = z.input<typeof repoQuerySchema>;
|
|
||||||
type RepoQuery = z.output<typeof repoQuerySchema>;
|
|
||||||
+8
-5
@@ -1,12 +1,15 @@
|
|||||||
export { App, type AppConfig, type CreateAppConfig } from "./App";
|
export { App, createApp, AppEvents, type AppConfig, type CreateAppConfig } from "./App";
|
||||||
|
|
||||||
export { MediaField } from "media/MediaField";
|
|
||||||
export {
|
export {
|
||||||
getDefaultConfig,
|
getDefaultConfig,
|
||||||
getDefaultSchema,
|
getDefaultSchema,
|
||||||
type ModuleConfigs,
|
type ModuleConfigs,
|
||||||
type ModuleSchemas
|
type ModuleSchemas,
|
||||||
} from "modules/ModuleManager";
|
type ModuleManagerOptions,
|
||||||
|
type ModuleBuildContext
|
||||||
|
} from "./modules/ModuleManager";
|
||||||
|
|
||||||
export * from "./adapter";
|
export { registries } from "modules/registries";
|
||||||
|
|
||||||
|
export type * from "./adapter";
|
||||||
export { Api, type ApiOptions } from "./Api";
|
export { Api, type ApiOptions } from "./Api";
|
||||||
|
|||||||
+10
-19
@@ -1,24 +1,15 @@
|
|||||||
|
import type { PrimaryFieldType } from "core";
|
||||||
import { EntityIndex, type EntityManager } from "data";
|
import { EntityIndex, type EntityManager } from "data";
|
||||||
import { type FileUploadedEventData, Storage, type StorageAdapter } from "media";
|
import { type FileUploadedEventData, Storage, type StorageAdapter } from "media";
|
||||||
import { Module } from "modules/Module";
|
import { Module } from "modules/Module";
|
||||||
import {
|
import { type FieldSchema, boolean, datetime, entity, json, number, text } from "../data/prototype";
|
||||||
type FieldSchema,
|
|
||||||
type InferFields,
|
|
||||||
type Schema,
|
|
||||||
boolean,
|
|
||||||
datetime,
|
|
||||||
entity,
|
|
||||||
json,
|
|
||||||
number,
|
|
||||||
text
|
|
||||||
} from "../data/prototype";
|
|
||||||
import { MediaController } from "./api/MediaController";
|
import { MediaController } from "./api/MediaController";
|
||||||
import { ADAPTERS, buildMediaSchema, type mediaConfigSchema, registry } from "./media-schema";
|
import { ADAPTERS, buildMediaSchema, type mediaConfigSchema, registry } from "./media-schema";
|
||||||
|
|
||||||
export type MediaFieldSchema = FieldSchema<typeof AppMedia.mediaFields>;
|
export type MediaFieldSchema = FieldSchema<typeof AppMedia.mediaFields>;
|
||||||
declare global {
|
declare module "core" {
|
||||||
interface DB {
|
interface DB {
|
||||||
media: MediaFieldSchema;
|
media: { id: PrimaryFieldType } & MediaFieldSchema;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,14 +103,14 @@ export class AppMedia extends Module<typeof mediaConfigSchema> {
|
|||||||
return this.em.entity(entity_name);
|
return this.em.entity(entity_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
get em(): EntityManager<DB> {
|
get em(): EntityManager {
|
||||||
return this.ctx.em;
|
return this.ctx.em;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupListeners() {
|
private setupListeners() {
|
||||||
//const media = this._entity;
|
//const media = this._entity;
|
||||||
const { emgr, em } = this.ctx;
|
const { emgr, em } = this.ctx;
|
||||||
const media = this.getMediaEntity();
|
const media = this.getMediaEntity().name as "media";
|
||||||
|
|
||||||
// when file is uploaded, sync with media entity
|
// when file is uploaded, sync with media entity
|
||||||
// @todo: need a way for singleton events!
|
// @todo: need a way for singleton events!
|
||||||
@@ -140,10 +131,10 @@ export class AppMedia extends Module<typeof mediaConfigSchema> {
|
|||||||
Storage.Events.FileDeletedEvent,
|
Storage.Events.FileDeletedEvent,
|
||||||
async (e) => {
|
async (e) => {
|
||||||
// simple file deletion sync
|
// simple file deletion sync
|
||||||
const item = await em.repo(media).findOne({ path: e.params.name });
|
const { data } = await em.repo(media).findOne({ path: e.params.name });
|
||||||
if (item.data) {
|
if (data) {
|
||||||
console.log("item.data", item.data);
|
console.log("item.data", data);
|
||||||
await em.mutator(media).deleteOne(item.data.id);
|
await em.mutator(media).deleteOne(data.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("App:storage:file deleted", e);
|
console.log("App:storage:file deleted", e);
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFiles() {
|
getFiles() {
|
||||||
return this.get(["files"]);
|
return this.get(["files"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFile(filename: string) {
|
getFile(filename: string) {
|
||||||
return this.get(["file", filename]);
|
return this.get(["file", filename]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,13 +32,13 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadFile(file: File) {
|
uploadFile(file: File) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
return this.post(["upload"], formData);
|
return this.post(["upload"], formData);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteFile(filename: string) {
|
deleteFile(filename: string) {
|
||||||
return this.delete(["file", filename]);
|
return this.delete(["file", filename]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -174,14 +174,14 @@ export class MediaController implements ClassController {
|
|||||||
const result = await mutator.insertOne({
|
const result = await mutator.insertOne({
|
||||||
...this.media.uploadedEventDataToMediaPayload(info),
|
...this.media.uploadedEventDataToMediaPayload(info),
|
||||||
...mediaRef
|
...mediaRef
|
||||||
});
|
} as any);
|
||||||
mutator.__unstable_toggleSystemEntityCreation(true);
|
mutator.__unstable_toggleSystemEntityCreation(true);
|
||||||
|
|
||||||
// delete items if needed
|
// delete items if needed
|
||||||
if (ids_to_delete.length > 0) {
|
if (ids_to_delete.length > 0) {
|
||||||
await this.media.em
|
await this.media.em
|
||||||
.mutator(mediaEntity)
|
.mutator(mediaEntity)
|
||||||
.deleteMany({ [id_field]: { $in: ids_to_delete } });
|
.deleteWhere({ [id_field]: { $in: ids_to_delete } });
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.json({ ok: true, result: result.data, ...info });
|
return c.json({ ok: true, result: result.data, ...info });
|
||||||
|
|||||||
+6
-14
@@ -17,10 +17,6 @@ import {
|
|||||||
import { type S3AdapterConfig, StorageS3Adapter } from "./storage/adapters/StorageS3Adapter";
|
import { type S3AdapterConfig, StorageS3Adapter } from "./storage/adapters/StorageS3Adapter";
|
||||||
|
|
||||||
export { StorageS3Adapter, type S3AdapterConfig, StorageCloudinaryAdapter, type CloudinaryConfig };
|
export { StorageS3Adapter, type S3AdapterConfig, StorageCloudinaryAdapter, type CloudinaryConfig };
|
||||||
/*export {
|
|
||||||
StorageLocalAdapter,
|
|
||||||
type LocalAdapterConfig
|
|
||||||
} from "./storage/adapters/StorageLocalAdapter";*/
|
|
||||||
|
|
||||||
export * as StorageEvents from "./storage/events";
|
export * as StorageEvents from "./storage/events";
|
||||||
export { type FileUploadedEventData } from "./storage/events";
|
export { type FileUploadedEventData } from "./storage/events";
|
||||||
@@ -31,16 +27,12 @@ type ClassThatImplements<T> = Constructor<T> & { prototype: T };
|
|||||||
export const MediaAdapterRegistry = new Registry<{
|
export const MediaAdapterRegistry = new Registry<{
|
||||||
cls: ClassThatImplements<StorageAdapter>;
|
cls: ClassThatImplements<StorageAdapter>;
|
||||||
schema: TObject;
|
schema: TObject;
|
||||||
}>().set({
|
}>((cls: ClassThatImplements<StorageAdapter>) => ({
|
||||||
s3: {
|
cls,
|
||||||
cls: StorageS3Adapter,
|
schema: cls.prototype.getSchema() as TObject
|
||||||
schema: StorageS3Adapter.prototype.getSchema()
|
}))
|
||||||
},
|
.register("s3", StorageS3Adapter)
|
||||||
cloudinary: {
|
.register("cloudinary", StorageCloudinaryAdapter);
|
||||||
cls: StorageCloudinaryAdapter,
|
|
||||||
schema: StorageCloudinaryAdapter.prototype.getSchema()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const Adapters = {
|
export const Adapters = {
|
||||||
s3: {
|
s3: {
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
import { readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
|
import { readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
|
||||||
import { type Static, Type, parse } from "core/utils";
|
import { type Static, Type, parse } from "core/utils";
|
||||||
import type {
|
import type { FileBody, FileListObject, FileMeta, StorageAdapter } from "../../Storage";
|
||||||
FileBody,
|
import { guess } from "../../mime-types-tiny";
|
||||||
FileListObject,
|
|
||||||
FileMeta,
|
|
||||||
FileUploadPayload,
|
|
||||||
StorageAdapter
|
|
||||||
} from "../../Storage";
|
|
||||||
import { guessMimeType } from "../../mime-types";
|
|
||||||
|
|
||||||
export const localAdapterConfig = Type.Object(
|
export const localAdapterConfig = Type.Object(
|
||||||
{
|
{
|
||||||
path: Type.String()
|
path: Type.String({ default: "./" })
|
||||||
},
|
},
|
||||||
{ title: "Local" }
|
{ title: "Local" }
|
||||||
);
|
);
|
||||||
@@ -89,7 +83,7 @@ export class StorageLocalAdapter implements StorageAdapter {
|
|||||||
async getObject(key: string, headers: Headers): Promise<Response> {
|
async getObject(key: string, headers: Headers): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const content = await readFile(`${this.config.path}/${key}`);
|
const content = await readFile(`${this.config.path}/${key}`);
|
||||||
const mimeType = guessMimeType(key);
|
const mimeType = guess(key);
|
||||||
|
|
||||||
return new Response(content, {
|
return new Response(content, {
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -111,7 +105,7 @@ export class StorageLocalAdapter implements StorageAdapter {
|
|||||||
async getObjectMeta(key: string): Promise<FileMeta> {
|
async getObjectMeta(key: string): Promise<FileMeta> {
|
||||||
const stats = await stat(`${this.config.path}/${key}`);
|
const stats = await stat(`${this.config.path}/${key}`);
|
||||||
return {
|
return {
|
||||||
type: guessMimeType(key) || "application/octet-stream",
|
type: guess(key) || "application/octet-stream",
|
||||||
size: stats.size
|
size: stats.size
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
export const Q = {
|
||||||
|
video: ["mp4", "webm"],
|
||||||
|
audio: ["ogg"],
|
||||||
|
image: ["jpeg", "png", "gif", "webp", "bmp", "tiff"],
|
||||||
|
text: ["html", "css", "mdx", "yaml", "vcard", "csv", "vtt"],
|
||||||
|
application: ["zip", "xml", "toml", "json", "json5"],
|
||||||
|
font: ["woff", "woff2", "ttf", "otf"]
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// reduced
|
||||||
|
const c = {
|
||||||
|
vnd: "vnd.openxmlformats-officedocument",
|
||||||
|
z: "application/x-7z-compressed",
|
||||||
|
t: (w = "plain") => `text/${w}`,
|
||||||
|
a: (w = "octet-stream") => `application/${w}`,
|
||||||
|
i: (w) => `image/${w}`,
|
||||||
|
v: (w) => `video/${w}`
|
||||||
|
} as const;
|
||||||
|
export const M = new Map<string, string>([
|
||||||
|
["7z", c.z],
|
||||||
|
["7zip", c.z],
|
||||||
|
["ai", c.a("pdf")],
|
||||||
|
["apk", c.a("vnd.android.package-archive")],
|
||||||
|
["doc", c.a("msword")],
|
||||||
|
["docx", `${c.vnd}.wordprocessingml.document`],
|
||||||
|
["eps", c.a("postscript")],
|
||||||
|
["epub", c.a("epub+zip")],
|
||||||
|
["ini", c.t()],
|
||||||
|
["jar", c.a("java-archive")],
|
||||||
|
["jsonld", c.a("ld+json")],
|
||||||
|
["jpg", c.i("jpeg")],
|
||||||
|
["log", c.t()],
|
||||||
|
["m3u", c.t()],
|
||||||
|
["m3u8", c.a("vnd.apple.mpegurl")],
|
||||||
|
["manifest", c.t("cache-manifest")],
|
||||||
|
["md", c.t("markdown")],
|
||||||
|
["mkv", c.v("x-matroska")],
|
||||||
|
["mp3", c.a("mpeg")],
|
||||||
|
["mobi", c.a("x-mobipocket-ebook")],
|
||||||
|
["ppt", c.a("powerpoint")],
|
||||||
|
["pptx", `${c.vnd}.presentationml.presentation`],
|
||||||
|
["qt", c.v("quicktime")],
|
||||||
|
["svg", c.i("svg+xml")],
|
||||||
|
["tif", c.i("tiff")],
|
||||||
|
["tsv", c.t("tab-separated-values")],
|
||||||
|
["tgz", c.a("x-tar")],
|
||||||
|
["txt", c.t()],
|
||||||
|
["text", c.t()],
|
||||||
|
["vcd", c.a("x-cdlink")],
|
||||||
|
["vcs", c.t("x-vcalendar")],
|
||||||
|
["wav", c.a("x-wav")],
|
||||||
|
["webmanifest", c.a("manifest+json")],
|
||||||
|
["xls", c.a("vnd.ms-excel")],
|
||||||
|
["xlsx", `${c.vnd}.spreadsheetml.sheet`],
|
||||||
|
["yml", c.t("yaml")]
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function guess(f: string): string {
|
||||||
|
try {
|
||||||
|
const e = f.split(".").pop() as string;
|
||||||
|
if (!e) {
|
||||||
|
return c.a();
|
||||||
|
}
|
||||||
|
|
||||||
|
// try quick first
|
||||||
|
for (const [t, _e] of Object.entries(Q)) {
|
||||||
|
// @ts-ignore
|
||||||
|
if (_e.includes(e)) {
|
||||||
|
return `${t}/${e}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return M.get(e!) as string;
|
||||||
|
} catch (e) {
|
||||||
|
return c.a();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import type { Hono } from "hono";
|
|||||||
export type ModuleBuildContext = {
|
export type ModuleBuildContext = {
|
||||||
connection: Connection;
|
connection: Connection;
|
||||||
server: Hono<any>;
|
server: Hono<any>;
|
||||||
em: EntityManager<any>;
|
em: EntityManager;
|
||||||
emgr: EventManager<any>;
|
emgr: EventManager<any>;
|
||||||
guard: Guard;
|
guard: Guard;
|
||||||
};
|
};
|
||||||
|
|||||||
+166
-62
@@ -1,4 +1,4 @@
|
|||||||
import type { PrimaryFieldType } from "core";
|
import { type PrimaryFieldType, isDebug } from "core";
|
||||||
import { encodeSearch } from "core/utils";
|
import { encodeSearch } from "core/utils";
|
||||||
|
|
||||||
export type { PrimaryFieldType };
|
export type { PrimaryFieldType };
|
||||||
@@ -10,6 +10,7 @@ export type BaseModuleApiOptions = {
|
|||||||
token_transport?: "header" | "cookie" | "none";
|
token_transport?: "header" | "cookie" | "none";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** @deprecated */
|
||||||
export type ApiResponse<Data = any> = {
|
export type ApiResponse<Data = any> = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
status: number;
|
status: number;
|
||||||
@@ -18,7 +19,11 @@ export type ApiResponse<Data = any> = {
|
|||||||
res: Response;
|
res: Response;
|
||||||
};
|
};
|
||||||
|
|
||||||
export abstract class ModuleApi<Options extends BaseModuleApiOptions> {
|
export type TInput = string | (string | number | PrimaryFieldType)[];
|
||||||
|
|
||||||
|
export abstract class ModuleApi<Options extends BaseModuleApiOptions = BaseModuleApiOptions> {
|
||||||
|
protected fetcher?: typeof fetch;
|
||||||
|
|
||||||
constructor(protected readonly _options: Partial<Options> = {}) {}
|
constructor(protected readonly _options: Partial<Options> = {}) {}
|
||||||
|
|
||||||
protected getDefaultOptions(): Partial<Options> {
|
protected getDefaultOptions(): Partial<Options> {
|
||||||
@@ -35,14 +40,15 @@ export abstract class ModuleApi<Options extends BaseModuleApiOptions> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected getUrl(path: string) {
|
protected getUrl(path: string) {
|
||||||
return this.options.host + (this.options.basepath + "/" + path).replace(/\/\//g, "/");
|
const basepath = this.options.basepath ?? "";
|
||||||
|
return this.options.host + (basepath + "/" + path).replace(/\/{2,}/g, "/").replace(/\/$/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async request<Data = any>(
|
protected request<Data = any>(
|
||||||
_input: string | (string | number | PrimaryFieldType)[],
|
_input: TInput,
|
||||||
_query?: Record<string, any> | URLSearchParams,
|
_query?: Record<string, any> | URLSearchParams,
|
||||||
_init?: RequestInit
|
_init?: RequestInit
|
||||||
): Promise<ApiResponse<Data>> {
|
): FetchPromise<ResponseObject<Data>> {
|
||||||
const method = _init?.method ?? "GET";
|
const method = _init?.method ?? "GET";
|
||||||
const input = Array.isArray(_input) ? _input.join("/") : _input;
|
const input = Array.isArray(_input) ? _input.join("/") : _input;
|
||||||
let url = this.getUrl(input);
|
let url = this.getUrl(input);
|
||||||
@@ -78,14 +84,130 @@ export abstract class ModuleApi<Options extends BaseModuleApiOptions> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//console.log("url", url);
|
const request = new Request(url, {
|
||||||
const res = await fetch(url, {
|
|
||||||
..._init,
|
..._init,
|
||||||
method,
|
method,
|
||||||
body,
|
body,
|
||||||
headers
|
headers
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return new FetchPromise(request, {
|
||||||
|
fetcher: this.fetcher
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get<Data = any>(
|
||||||
|
_input: TInput,
|
||||||
|
_query?: Record<string, any> | URLSearchParams,
|
||||||
|
_init?: RequestInit
|
||||||
|
) {
|
||||||
|
return this.request<Data>(_input, _query, {
|
||||||
|
..._init,
|
||||||
|
method: "GET"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
post<Data = any>(_input: TInput, body?: any, _init?: RequestInit) {
|
||||||
|
return this.request<Data>(_input, undefined, {
|
||||||
|
..._init,
|
||||||
|
body,
|
||||||
|
method: "POST"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
patch<Data = any>(_input: TInput, body?: any, _init?: RequestInit) {
|
||||||
|
return this.request<Data>(_input, undefined, {
|
||||||
|
..._init,
|
||||||
|
body,
|
||||||
|
method: "PATCH"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
put<Data = any>(_input: TInput, body?: any, _init?: RequestInit) {
|
||||||
|
return this.request<Data>(_input, undefined, {
|
||||||
|
..._init,
|
||||||
|
body,
|
||||||
|
method: "PUT"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
delete<Data = any>(_input: TInput, _init?: RequestInit) {
|
||||||
|
return this.request<Data>(_input, undefined, {
|
||||||
|
..._init,
|
||||||
|
method: "DELETE"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ResponseObject<Body = any, Data = Body extends { data: infer R } ? R : Body> = Data & {
|
||||||
|
raw: Response;
|
||||||
|
res: Response;
|
||||||
|
data: Data;
|
||||||
|
body: Body;
|
||||||
|
ok: boolean;
|
||||||
|
status: number;
|
||||||
|
toJSON(): Data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createResponseProxy<Body = any, Data = any>(
|
||||||
|
raw: Response,
|
||||||
|
body: Body,
|
||||||
|
data?: Data
|
||||||
|
): ResponseObject<Body, Data> {
|
||||||
|
const actualData = data ?? (body as unknown as Data);
|
||||||
|
const _props = ["raw", "body", "ok", "status", "res", "data", "toJSON"];
|
||||||
|
|
||||||
|
return new Proxy(actualData as any, {
|
||||||
|
get(target, prop, receiver) {
|
||||||
|
if (prop === "raw" || prop === "res") return raw;
|
||||||
|
if (prop === "body") return body;
|
||||||
|
if (prop === "data") return data;
|
||||||
|
if (prop === "ok") return raw.ok;
|
||||||
|
if (prop === "status") return raw.status;
|
||||||
|
if (prop === "toJSON") {
|
||||||
|
return () => target;
|
||||||
|
}
|
||||||
|
return Reflect.get(target, prop, receiver);
|
||||||
|
},
|
||||||
|
has(target, prop) {
|
||||||
|
if (_props.includes(prop as string)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return Reflect.has(target, prop);
|
||||||
|
},
|
||||||
|
ownKeys(target) {
|
||||||
|
return Array.from(new Set([...Reflect.ownKeys(target), ..._props]));
|
||||||
|
},
|
||||||
|
getOwnPropertyDescriptor(target, prop) {
|
||||||
|
if (_props.includes(prop as string)) {
|
||||||
|
return {
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
value: Reflect.get({ raw, body, ok: raw.ok, status: raw.status }, prop)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return Reflect.getOwnPropertyDescriptor(target, prop);
|
||||||
|
}
|
||||||
|
}) as ResponseObject<Body, Data>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FetchPromise<T = ApiResponse<any>> implements Promise<T> {
|
||||||
|
// @ts-ignore
|
||||||
|
[Symbol.toStringTag]: "FetchPromise";
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public request: Request,
|
||||||
|
protected options?: {
|
||||||
|
fetcher?: typeof fetch;
|
||||||
|
}
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute(): Promise<ResponseObject<T>> {
|
||||||
|
// delay in dev environment
|
||||||
|
isDebug() && (await new Promise((resolve) => setTimeout(resolve, 200)));
|
||||||
|
|
||||||
|
const fetcher = this.options?.fetcher ?? fetch;
|
||||||
|
const res = await fetcher(this.request);
|
||||||
let resBody: any;
|
let resBody: any;
|
||||||
let resData: any;
|
let resData: any;
|
||||||
|
|
||||||
@@ -99,69 +221,51 @@ export abstract class ModuleApi<Options extends BaseModuleApiOptions> {
|
|||||||
resBody = await res.text();
|
resBody = await res.text();
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return createResponseProxy<T>(res, resBody, resData);
|
||||||
success: res.ok,
|
|
||||||
status: res.status,
|
|
||||||
body: resBody,
|
|
||||||
data: resData,
|
|
||||||
res
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async get<Data = any>(
|
// biome-ignore lint/suspicious/noThenProperty: it's a promise :)
|
||||||
_input: string | (string | number | PrimaryFieldType)[],
|
then<TResult1 = T, TResult2 = never>(
|
||||||
_query?: Record<string, any> | URLSearchParams,
|
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null | undefined,
|
||||||
_init?: RequestInit
|
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined
|
||||||
) {
|
): Promise<TResult1 | TResult2> {
|
||||||
return this.request<Data>(_input, _query, {
|
return this.execute().then(onfulfilled as any, onrejected);
|
||||||
..._init,
|
|
||||||
method: "GET"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async post<Data = any>(
|
catch<TResult = never>(
|
||||||
_input: string | (string | number | PrimaryFieldType)[],
|
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null | undefined
|
||||||
body?: any,
|
): Promise<T | TResult> {
|
||||||
_init?: RequestInit
|
return this.then(undefined, onrejected);
|
||||||
) {
|
|
||||||
return this.request<Data>(_input, undefined, {
|
|
||||||
..._init,
|
|
||||||
body,
|
|
||||||
method: "POST"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async patch<Data = any>(
|
finally(onfinally?: (() => void) | null | undefined): Promise<T> {
|
||||||
_input: string | (string | number | PrimaryFieldType)[],
|
return this.then(
|
||||||
body?: any,
|
(value) => {
|
||||||
_init?: RequestInit
|
onfinally?.();
|
||||||
) {
|
return value;
|
||||||
return this.request<Data>(_input, undefined, {
|
},
|
||||||
..._init,
|
(reason) => {
|
||||||
body,
|
onfinally?.();
|
||||||
method: "PATCH"
|
throw reason;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async put<Data = any>(
|
path(): string {
|
||||||
_input: string | (string | number | PrimaryFieldType)[],
|
const url = new URL(this.request.url);
|
||||||
body?: any,
|
return url.pathname;
|
||||||
_init?: RequestInit
|
|
||||||
) {
|
|
||||||
return this.request<Data>(_input, undefined, {
|
|
||||||
..._init,
|
|
||||||
body,
|
|
||||||
method: "PUT"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async delete<Data = any>(
|
key(options?: { search: boolean }): string {
|
||||||
_input: string | (string | number | PrimaryFieldType)[],
|
const url = new URL(this.request.url);
|
||||||
_init?: RequestInit
|
return options?.search !== false ? this.path() + url.search : this.path();
|
||||||
) {
|
}
|
||||||
return this.request<Data>(_input, undefined, {
|
|
||||||
..._init,
|
keyArray(options?: { search: boolean }): string[] {
|
||||||
method: "DELETE"
|
const url = new URL(this.request.url);
|
||||||
});
|
const path = this.path().split("/");
|
||||||
|
return (options?.search !== false ? [...path, url.searchParams.toString()] : path).filter(
|
||||||
|
Boolean
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+218
-111
@@ -1,12 +1,33 @@
|
|||||||
import { Diff } from "@sinclair/typebox/value";
|
|
||||||
import { Guard } from "auth";
|
import { Guard } from "auth";
|
||||||
import { DebugLogger, isDebug } from "core";
|
import { BkndError, DebugLogger } from "core";
|
||||||
import { EventManager } from "core/events";
|
import { EventManager } from "core/events";
|
||||||
import { Default, type Static, objectEach, transformObject } from "core/utils";
|
import { clone, diff } from "core/object/diff";
|
||||||
import { type Connection, EntityManager } from "data";
|
import {
|
||||||
|
Default,
|
||||||
|
type Static,
|
||||||
|
StringEnum,
|
||||||
|
Type,
|
||||||
|
mark,
|
||||||
|
objectEach,
|
||||||
|
stripMark,
|
||||||
|
transformObject,
|
||||||
|
withDisabledConsole
|
||||||
|
} from "core/utils";
|
||||||
|
import {
|
||||||
|
type Connection,
|
||||||
|
EntityManager,
|
||||||
|
type Schema,
|
||||||
|
datetime,
|
||||||
|
entity,
|
||||||
|
enumm,
|
||||||
|
jsonSchema,
|
||||||
|
number
|
||||||
|
} from "data";
|
||||||
|
import { TransformPersistFailedException } from "data/errors";
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { type Kysely, sql } from "kysely";
|
import type { Kysely } from "kysely";
|
||||||
import { CURRENT_VERSION, TABLE_NAME, migrate, migrateSchema } from "modules/migrations";
|
import { mergeWith } from "lodash-es";
|
||||||
|
import { CURRENT_VERSION, TABLE_NAME, migrate } from "modules/migrations";
|
||||||
import { AppServer } from "modules/server/AppServer";
|
import { AppServer } from "modules/server/AppServer";
|
||||||
import { AppAuth } from "../auth/AppAuth";
|
import { AppAuth } from "../auth/AppAuth";
|
||||||
import { AppData } from "../data/AppData";
|
import { AppData } from "../data/AppData";
|
||||||
@@ -14,9 +35,11 @@ import { AppFlows } from "../flows/AppFlows";
|
|||||||
import { AppMedia } from "../media/AppMedia";
|
import { AppMedia } from "../media/AppMedia";
|
||||||
import type { Module, ModuleBuildContext } from "./Module";
|
import type { Module, ModuleBuildContext } from "./Module";
|
||||||
|
|
||||||
|
export type { ModuleBuildContext };
|
||||||
|
|
||||||
export const MODULES = {
|
export const MODULES = {
|
||||||
server: AppServer,
|
server: AppServer,
|
||||||
data: AppData<any>,
|
data: AppData,
|
||||||
auth: AppAuth,
|
auth: AppAuth,
|
||||||
media: AppMedia,
|
media: AppMedia,
|
||||||
flows: AppFlows
|
flows: AppFlows
|
||||||
@@ -37,10 +60,13 @@ export type ModuleSchemas = {
|
|||||||
export type ModuleConfigs = {
|
export type ModuleConfigs = {
|
||||||
[K in keyof ModuleSchemas]: Static<ModuleSchemas[K]>;
|
[K in keyof ModuleSchemas]: Static<ModuleSchemas[K]>;
|
||||||
};
|
};
|
||||||
|
type PartialRec<T> = { [P in keyof T]?: PartialRec<T[P]> };
|
||||||
|
|
||||||
export type InitialModuleConfigs = {
|
export type InitialModuleConfigs =
|
||||||
version: number;
|
| ({
|
||||||
} & Partial<ModuleConfigs>;
|
version: number;
|
||||||
|
} & ModuleConfigs)
|
||||||
|
| PartialRec<ModuleConfigs>;
|
||||||
|
|
||||||
export type ModuleManagerOptions = {
|
export type ModuleManagerOptions = {
|
||||||
initial?: InitialModuleConfigs;
|
initial?: InitialModuleConfigs;
|
||||||
@@ -49,8 +75,14 @@ export type ModuleManagerOptions = {
|
|||||||
module: Module,
|
module: Module,
|
||||||
config: ModuleConfigs[Module]
|
config: ModuleConfigs[Module]
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
// triggered when no config table existed
|
||||||
|
onFirstBoot?: () => Promise<void>;
|
||||||
// base path for the hono instance
|
// base path for the hono instance
|
||||||
basePath?: string;
|
basePath?: string;
|
||||||
|
// doesn't perform validity checks for given/fetched config
|
||||||
|
trustFetched?: boolean;
|
||||||
|
// runs when initial config provided on a fresh database
|
||||||
|
seed?: (ctx: ModuleBuildContext) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ConfigTable<Json = ModuleConfigs> = {
|
type ConfigTable<Json = ModuleConfigs> = {
|
||||||
@@ -61,9 +93,37 @@ type ConfigTable<Json = ModuleConfigs> = {
|
|||||||
updated_at?: Date;
|
updated_at?: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const configJsonSchema = Type.Union([
|
||||||
|
getDefaultSchema(),
|
||||||
|
Type.Array(
|
||||||
|
Type.Object({
|
||||||
|
t: StringEnum(["a", "r", "e"]),
|
||||||
|
p: Type.Array(Type.Union([Type.String(), Type.Number()])),
|
||||||
|
o: Type.Optional(Type.Any()),
|
||||||
|
n: Type.Optional(Type.Any())
|
||||||
|
})
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
const __bknd = entity(TABLE_NAME, {
|
||||||
|
version: number().required(),
|
||||||
|
type: enumm({ enum: ["config", "diff", "backup"] }).required(),
|
||||||
|
json: jsonSchema({ schema: configJsonSchema }).required(),
|
||||||
|
created_at: datetime(),
|
||||||
|
updated_at: datetime()
|
||||||
|
});
|
||||||
|
type ConfigTable2 = Schema<typeof __bknd>;
|
||||||
|
interface T_INTERNAL_EM {
|
||||||
|
__bknd: ConfigTable2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @todo: cleanup old diffs on upgrade
|
||||||
|
// @todo: cleanup multiple backups on upgrade
|
||||||
export class ModuleManager {
|
export class ModuleManager {
|
||||||
private modules: Modules;
|
private modules: Modules;
|
||||||
em!: EntityManager<any>;
|
// internal em for __bknd config table
|
||||||
|
__em!: EntityManager<T_INTERNAL_EM>;
|
||||||
|
// ctx for modules
|
||||||
|
em!: EntityManager;
|
||||||
server!: Hono;
|
server!: Hono;
|
||||||
emgr!: EventManager;
|
emgr!: EventManager;
|
||||||
guard!: Guard;
|
guard!: Guard;
|
||||||
@@ -71,28 +131,32 @@ export class ModuleManager {
|
|||||||
private _version: number = 0;
|
private _version: number = 0;
|
||||||
private _built = false;
|
private _built = false;
|
||||||
private _fetched = false;
|
private _fetched = false;
|
||||||
private readonly _provided;
|
|
||||||
|
|
||||||
private logger = new DebugLogger(isDebug() && false);
|
// @todo: keep? not doing anything with it
|
||||||
|
private readonly _booted_with?: "provided" | "partial";
|
||||||
|
|
||||||
|
private logger = new DebugLogger(false);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly connection: Connection,
|
private readonly connection: Connection,
|
||||||
private options?: Partial<ModuleManagerOptions>
|
private options?: Partial<ModuleManagerOptions>
|
||||||
) {
|
) {
|
||||||
|
this.__em = new EntityManager([__bknd], this.connection);
|
||||||
this.modules = {} as Modules;
|
this.modules = {} as Modules;
|
||||||
this.emgr = new EventManager();
|
this.emgr = new EventManager();
|
||||||
const context = this.ctx(true);
|
const context = this.ctx(true);
|
||||||
let initial = {} as Partial<ModuleConfigs>;
|
let initial = {} as Partial<ModuleConfigs>;
|
||||||
|
|
||||||
if (options?.initial) {
|
if (options?.initial) {
|
||||||
const { version, ...initialConfig } = options.initial;
|
if ("version" in options.initial) {
|
||||||
if (version && initialConfig) {
|
const { version, ...initialConfig } = options.initial;
|
||||||
this._version = version;
|
this._version = version;
|
||||||
initial = initialConfig;
|
initial = stripMark(initialConfig);
|
||||||
|
|
||||||
this._provided = true;
|
this._booted_with = "provided";
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Initial was provided, but it needs a version!");
|
initial = mergeWith(getDefaultConfig(), options.initial);
|
||||||
|
this._booted_with = "partial";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +184,25 @@ export class ModuleManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private repo() {
|
||||||
|
return this.__em.repo(__bknd);
|
||||||
|
}
|
||||||
|
|
||||||
|
private mutator() {
|
||||||
|
return this.__em.mutator(__bknd);
|
||||||
|
}
|
||||||
|
|
||||||
|
private get db() {
|
||||||
|
return this.connection.kysely as Kysely<{ table: ConfigTable }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncConfigTable() {
|
||||||
|
this.logger.context("sync").log("start");
|
||||||
|
const result = await this.__em.schema().sync({ force: true });
|
||||||
|
this.logger.log("done").clear();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private rebuildServer() {
|
private rebuildServer() {
|
||||||
this.server = new Hono();
|
this.server = new Hono();
|
||||||
if (this.options?.basePath) {
|
if (this.options?.basePath) {
|
||||||
@@ -153,27 +236,26 @@ export class ModuleManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private get db() {
|
|
||||||
return this.connection.kysely as Kysely<{ table: ConfigTable }>;
|
|
||||||
}
|
|
||||||
|
|
||||||
get table() {
|
|
||||||
return TABLE_NAME as "table";
|
|
||||||
}
|
|
||||||
|
|
||||||
private async fetch(): Promise<ConfigTable> {
|
private async fetch(): Promise<ConfigTable> {
|
||||||
this.logger.context("fetch").log("fetching");
|
this.logger.context("fetch").log("fetching");
|
||||||
|
|
||||||
const startTime = performance.now();
|
// disabling console log, because the table might not exist yet
|
||||||
const result = await this.db
|
return await withDisabledConsole(async () => {
|
||||||
.selectFrom(this.table)
|
const startTime = performance.now();
|
||||||
.selectAll()
|
const { data: result } = await this.repo().findOne(
|
||||||
.where("type", "=", "config")
|
{ type: "config" },
|
||||||
.orderBy("version", "desc")
|
{
|
||||||
.executeTakeFirstOrThrow();
|
sort: { by: "version", dir: "desc" }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
this.logger.log("took", performance.now() - startTime, "ms", result).clear();
|
if (!result) {
|
||||||
return result;
|
throw BkndError.with("no config");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log("took", performance.now() - startTime, "ms", result.version).clear();
|
||||||
|
return result as ConfigTable;
|
||||||
|
}, ["log", "error", "warn"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async save() {
|
async save() {
|
||||||
@@ -181,65 +263,75 @@ export class ModuleManager {
|
|||||||
const configs = this.configs();
|
const configs = this.configs();
|
||||||
const version = this.version();
|
const version = this.version();
|
||||||
|
|
||||||
const json = JSON.stringify(configs) as any;
|
try {
|
||||||
const state = await this.fetch();
|
const state = await this.fetch();
|
||||||
|
this.logger.log("fetched version", state.version);
|
||||||
|
|
||||||
if (state.version !== version) {
|
if (state.version !== version) {
|
||||||
// @todo: mark all others as "backup"
|
// @todo: mark all others as "backup"
|
||||||
this.logger.log("version conflict, storing new version", state.version, version);
|
this.logger.log("version conflict, storing new version", state.version, version);
|
||||||
await this.db
|
await this.mutator().insertOne({
|
||||||
.insertInto(this.table)
|
version: state.version,
|
||||||
.values({
|
type: "backup",
|
||||||
version,
|
json: configs
|
||||||
|
});
|
||||||
|
await this.mutator().insertOne({
|
||||||
|
version: version,
|
||||||
type: "config",
|
type: "config",
|
||||||
json
|
json: configs
|
||||||
})
|
});
|
||||||
.execute();
|
} else {
|
||||||
} else {
|
this.logger.log("version matches");
|
||||||
this.logger.log("version matches");
|
|
||||||
|
|
||||||
const diff = Diff(state.json, JSON.parse(json));
|
// clean configs because of Diff() function
|
||||||
this.logger.log("checking diff", diff);
|
const diffs = diff(state.json, clone(configs));
|
||||||
|
this.logger.log("checking diff", diffs);
|
||||||
|
|
||||||
if (diff.length > 0) {
|
if (diff.length > 0) {
|
||||||
// store diff
|
// store diff
|
||||||
await this.db
|
await this.mutator().insertOne({
|
||||||
.insertInto(this.table)
|
|
||||||
.values({
|
|
||||||
version,
|
version,
|
||||||
type: "diff",
|
type: "diff",
|
||||||
json: JSON.stringify(diff) as any
|
json: clone(diffs)
|
||||||
})
|
});
|
||||||
.execute();
|
|
||||||
|
|
||||||
await this.db
|
// store new version
|
||||||
.updateTable(this.table)
|
await this.mutator().updateWhere(
|
||||||
.set({ version, json, updated_at: sql`CURRENT_TIMESTAMP` })
|
{
|
||||||
.where((eb) => eb.and([eb("type", "=", "config"), eb("version", "=", version)]))
|
version,
|
||||||
.execute();
|
json: configs,
|
||||||
|
updated_at: new Date()
|
||||||
|
} as any,
|
||||||
|
{
|
||||||
|
type: "config",
|
||||||
|
version
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.logger.log("no diff, not saving");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof BkndError) {
|
||||||
|
this.logger.log("no config, just save fresh");
|
||||||
|
// no config, just save
|
||||||
|
await this.mutator().insertOne({
|
||||||
|
type: "config",
|
||||||
|
version,
|
||||||
|
json: configs,
|
||||||
|
created_at: new Date(),
|
||||||
|
updated_at: new Date()
|
||||||
|
});
|
||||||
|
} else if (e instanceof TransformPersistFailedException) {
|
||||||
|
console.error("Cannot save invalid config");
|
||||||
|
throw e;
|
||||||
} else {
|
} else {
|
||||||
this.logger.log("no diff, not saving");
|
console.error("Aborting");
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanup
|
// @todo: cleanup old versions?
|
||||||
/*this.logger.log("cleaning up");
|
|
||||||
const result = await this.db
|
|
||||||
.deleteFrom(this.table)
|
|
||||||
.where((eb) =>
|
|
||||||
eb.or([
|
|
||||||
// empty migrations
|
|
||||||
eb.and([
|
|
||||||
eb("type", "=", "config"),
|
|
||||||
eb("version", "<", version),
|
|
||||||
eb("json", "is", null)
|
|
||||||
]),
|
|
||||||
// past diffs
|
|
||||||
eb.and([eb("type", "=", "diff"), eb("version", "<", version)])
|
|
||||||
])
|
|
||||||
)
|
|
||||||
.executeTakeFirst();
|
|
||||||
this.logger.log("cleaned up", result.numDeletedRows);*/
|
|
||||||
|
|
||||||
this.logger.clear();
|
this.logger.clear();
|
||||||
return this;
|
return this;
|
||||||
@@ -250,6 +342,8 @@ export class ModuleManager {
|
|||||||
|
|
||||||
if (this.version() < CURRENT_VERSION) {
|
if (this.version() < CURRENT_VERSION) {
|
||||||
this.logger.log("there are migrations, verify version");
|
this.logger.log("there are migrations, verify version");
|
||||||
|
// sync __bknd table
|
||||||
|
await this.syncConfigTable();
|
||||||
|
|
||||||
// modules must be built before migration
|
// modules must be built before migration
|
||||||
await this.buildModules({ graceful: true });
|
await this.buildModules({ graceful: true });
|
||||||
@@ -264,14 +358,7 @@ export class ModuleManager {
|
|||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.logger.clear(); // fetch couldn't clear
|
this.logger.clear(); // fetch couldn't clear
|
||||||
|
throw new Error(`Version is ${this.version()}, fetch failed: ${e.message}`);
|
||||||
// if table doesn't exist, migrate schema to version
|
|
||||||
if (e.message.includes("no such table")) {
|
|
||||||
this.logger.log("table has to created, migrating schema up to", this.version());
|
|
||||||
await migrateSchema(this.version(), { db: this.db });
|
|
||||||
} else {
|
|
||||||
throw new Error(`Version is ${this.version()}, fetch failed: ${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log("now migrating");
|
this.logger.log("now migrating");
|
||||||
@@ -289,9 +376,6 @@ export class ModuleManager {
|
|||||||
configs = _configs;
|
configs = _configs;
|
||||||
|
|
||||||
this.setConfigs(configs);
|
this.setConfigs(configs);
|
||||||
/* objectEach(configs, (config, key) => {
|
|
||||||
this.get(key as any).setConfig(config);
|
|
||||||
}); */
|
|
||||||
|
|
||||||
this._version = version;
|
this._version = version;
|
||||||
this.logger.log("migrated to", version);
|
this.logger.log("migrated to", version);
|
||||||
@@ -325,10 +409,11 @@ export class ModuleManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.log("building");
|
||||||
const ctx = this.ctx(true);
|
const ctx = this.ctx(true);
|
||||||
for (const key in this.modules) {
|
for (const key in this.modules) {
|
||||||
this.logger.log(`building "${key}"`);
|
|
||||||
await this.modules[key].setContext(ctx).build();
|
await this.modules[key].setContext(ctx).build();
|
||||||
|
this.logger.log("built", key);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._built = true;
|
this._built = true;
|
||||||
@@ -337,37 +422,44 @@ export class ModuleManager {
|
|||||||
|
|
||||||
async build() {
|
async build() {
|
||||||
this.logger.context("build").log("version", this.version());
|
this.logger.context("build").log("version", this.version());
|
||||||
|
this.logger.log("booted with", this._booted_with);
|
||||||
|
|
||||||
// if no config provided, try fetch from db
|
// if no config provided, try fetch from db
|
||||||
if (this.version() === 0) {
|
if (this.version() === 0) {
|
||||||
this.logger.context("build no config").log("version is 0");
|
this.logger.context("no version").log("version is 0");
|
||||||
try {
|
try {
|
||||||
const result = await this.fetch();
|
const result = await this.fetch();
|
||||||
|
|
||||||
// set version and config from fetched
|
// set version and config from fetched
|
||||||
this._version = result.version;
|
this._version = result.version;
|
||||||
|
|
||||||
|
if (this.version() !== CURRENT_VERSION) {
|
||||||
|
await this.syncConfigTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options?.trustFetched === true) {
|
||||||
|
this.logger.log("trusting fetched config (mark)");
|
||||||
|
mark(result.json);
|
||||||
|
}
|
||||||
|
|
||||||
this.setConfigs(result.json);
|
this.setConfigs(result.json);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.logger.clear(); // fetch couldn't clear
|
this.logger.clear(); // fetch couldn't clear
|
||||||
|
|
||||||
this.logger.context("error handler").log("fetch failed", e.message);
|
this.logger.context("error handler").log("fetch failed", e.message);
|
||||||
// if table doesn't exist, migrate schema, set default config and latest version
|
|
||||||
if (e.message.includes("no such table")) {
|
|
||||||
this.logger.log("migrate schema to", CURRENT_VERSION);
|
|
||||||
await migrateSchema(CURRENT_VERSION, { db: this.db });
|
|
||||||
this._version = CURRENT_VERSION;
|
|
||||||
|
|
||||||
// we can safely build modules, since config version is up to date
|
// we can safely build modules, since config version is up to date
|
||||||
// it's up to date because we use default configs (no fetch result)
|
// it's up to date because we use default configs (no fetch result)
|
||||||
await this.buildModules();
|
this._version = CURRENT_VERSION;
|
||||||
await this.save();
|
await this.syncConfigTable();
|
||||||
|
await this.buildModules();
|
||||||
|
await this.save();
|
||||||
|
|
||||||
this.logger.clear();
|
// run initial setup
|
||||||
return this;
|
await this.setupInitial();
|
||||||
} else {
|
|
||||||
throw e;
|
this.logger.clear();
|
||||||
//throw new Error("Issues connecting to the database. Reason: " + e.message);
|
return this;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.logger.clear();
|
this.logger.clear();
|
||||||
}
|
}
|
||||||
@@ -380,6 +472,21 @@ export class ModuleManager {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async setupInitial() {
|
||||||
|
const ctx = {
|
||||||
|
...this.ctx(),
|
||||||
|
// disable events for initial setup
|
||||||
|
em: this.ctx().em.fork()
|
||||||
|
};
|
||||||
|
|
||||||
|
// perform a sync
|
||||||
|
await ctx.em.schema().sync({ force: true });
|
||||||
|
await this.options?.seed?.(ctx);
|
||||||
|
|
||||||
|
// run first boot event
|
||||||
|
await this.options?.onFirstBoot?.();
|
||||||
|
}
|
||||||
|
|
||||||
get<K extends keyof Modules>(key: K): Modules[K] {
|
get<K extends keyof Modules>(key: K): Modules[K] {
|
||||||
if (!(key in this.modules)) {
|
if (!(key in this.modules)) {
|
||||||
throw new Error(`Module "${key}" doesn't exist, cannot get`);
|
throw new Error(`Module "${key}" doesn't exist, cannot get`);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { ConfigUpdateResponse } from "modules/server/SystemController";
|
||||||
import { ModuleApi } from "./ModuleApi";
|
import { ModuleApi } from "./ModuleApi";
|
||||||
import type { ModuleConfigs, ModuleKey, ModuleSchemas } from "./ModuleManager";
|
import type { ModuleConfigs, ModuleKey, ModuleSchemas } from "./ModuleManager";
|
||||||
|
|
||||||
@@ -15,37 +16,41 @@ export class SystemApi extends ModuleApi<any> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async readSchema(options?: { config?: boolean; secrets?: boolean }) {
|
readConfig() {
|
||||||
return await this.get<ApiSchemaResponse>("schema", {
|
return this.get<{ version: number } & ModuleConfigs>("config");
|
||||||
|
}
|
||||||
|
|
||||||
|
readSchema(options?: { config?: boolean; secrets?: boolean }) {
|
||||||
|
return this.get<ApiSchemaResponse>("schema", {
|
||||||
config: options?.config ? 1 : 0,
|
config: options?.config ? 1 : 0,
|
||||||
secrets: options?.secrets ? 1 : 0
|
secrets: options?.secrets ? 1 : 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async setConfig<Module extends ModuleKey>(
|
setConfig<Module extends ModuleKey>(
|
||||||
module: Module,
|
module: Module,
|
||||||
value: ModuleConfigs[Module],
|
value: ModuleConfigs[Module],
|
||||||
force?: boolean
|
force?: boolean
|
||||||
) {
|
) {
|
||||||
return await this.post<any>(
|
return this.post<ConfigUpdateResponse>(
|
||||||
["config", "set", module].join("/") + `?force=${force ? 1 : 0}`,
|
["config", "set", module].join("/") + `?force=${force ? 1 : 0}`,
|
||||||
value
|
value
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addConfig<Module extends ModuleKey>(module: Module, path: string, value: any) {
|
addConfig<Module extends ModuleKey>(module: Module, path: string, value: any) {
|
||||||
return await this.post<any>(["config", "add", module, path], value);
|
return this.post<ConfigUpdateResponse>(["config", "add", module, path], value);
|
||||||
}
|
}
|
||||||
|
|
||||||
async patchConfig<Module extends ModuleKey>(module: Module, path: string, value: any) {
|
patchConfig<Module extends ModuleKey>(module: Module, path: string, value: any) {
|
||||||
return await this.patch<any>(["config", "patch", module, path], value);
|
return this.patch<ConfigUpdateResponse>(["config", "patch", module, path], value);
|
||||||
}
|
}
|
||||||
|
|
||||||
async overwriteConfig<Module extends ModuleKey>(module: Module, path: string, value: any) {
|
overwriteConfig<Module extends ModuleKey>(module: Module, path: string, value: any) {
|
||||||
return await this.put<any>(["config", "overwrite", module, path], value);
|
return this.put<ConfigUpdateResponse>(["config", "overwrite", module, path], value);
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeConfig<Module extends ModuleKey>(module: Module, path: string) {
|
removeConfig<Module extends ModuleKey>(module: Module, path: string) {
|
||||||
return await this.delete<any>(["config", "remove", module, path]);
|
return this.delete<ConfigUpdateResponse>(["config", "remove", module, path]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,26 +16,8 @@ export type Migration = {
|
|||||||
export const migrations: Migration[] = [
|
export const migrations: Migration[] = [
|
||||||
{
|
{
|
||||||
version: 1,
|
version: 1,
|
||||||
schema: true,
|
//schema: true,
|
||||||
up: async (config, { db }) => {
|
up: async (config) => config
|
||||||
//console.log("config given", config);
|
|
||||||
await db.schema
|
|
||||||
.createTable(TABLE_NAME)
|
|
||||||
.addColumn("id", "integer", (col) => col.primaryKey().notNull().autoIncrement())
|
|
||||||
.addColumn("version", "integer", (col) => col.notNull())
|
|
||||||
.addColumn("type", "text", (col) => col.notNull())
|
|
||||||
.addColumn("json", "text")
|
|
||||||
.addColumn("created_at", "datetime", (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`))
|
|
||||||
.addColumn("updated_at", "datetime", (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`))
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db
|
|
||||||
.insertInto(TABLE_NAME)
|
|
||||||
.values({ version: 1, type: "config", json: null })
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
version: 2,
|
version: 2,
|
||||||
@@ -45,12 +27,8 @@ export const migrations: Migration[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
version: 3,
|
version: 3,
|
||||||
schema: true,
|
//schema: true,
|
||||||
up: async (config, { db }) => {
|
up: async (config) => config
|
||||||
await db.schema.alterTable(TABLE_NAME).addColumn("deleted_at", "datetime").execute();
|
|
||||||
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
version: 4,
|
version: 4,
|
||||||
@@ -94,6 +72,13 @@ export const migrations: Migration[] = [
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/*{
|
||||||
|
version: 8,
|
||||||
|
up: async (config, { db }) => {
|
||||||
|
await db.deleteFrom(TABLE_NAME).where("type", "=", "diff").execute();
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}*/
|
||||||
];
|
];
|
||||||
|
|
||||||
export const CURRENT_VERSION = migrations[migrations.length - 1]?.version ?? 0;
|
export const CURRENT_VERSION = migrations[migrations.length - 1]?.version ?? 0;
|
||||||
@@ -127,28 +112,6 @@ export async function migrateTo(
|
|||||||
return [version, updated];
|
return [version, updated];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function migrateSchema(to: number, ctx: MigrationContext, current: number = 0) {
|
|
||||||
console.log("migrating SCHEMA to", to, "from", current);
|
|
||||||
const todo = migrations.filter((m) => m.version > current && m.version <= to && m.schema);
|
|
||||||
console.log("todo", todo.length);
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
let version = 0;
|
|
||||||
for (const migration of todo) {
|
|
||||||
console.log("-- running migration", i + 1, "of", todo.length);
|
|
||||||
try {
|
|
||||||
await migration.up({}, ctx);
|
|
||||||
version = migration.version;
|
|
||||||
i++;
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
throw new Error(`Migration ${migration.version} failed: ${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return version;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function migrate(
|
export async function migrate(
|
||||||
current: number,
|
current: number,
|
||||||
config: GenericConfigObject,
|
config: GenericConfigObject,
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ import * as SystemPermissions from "modules/permissions";
|
|||||||
|
|
||||||
const htmlBkndContextReplace = "<!-- BKND_CONTEXT -->";
|
const htmlBkndContextReplace = "<!-- BKND_CONTEXT -->";
|
||||||
|
|
||||||
|
// @todo: add migration to remove admin path from config
|
||||||
export type AdminControllerOptions = {
|
export type AdminControllerOptions = {
|
||||||
|
basepath?: string;
|
||||||
html?: string;
|
html?: string;
|
||||||
forceDev?: boolean;
|
forceDev?: boolean | { mainPath: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
export class AdminController implements ClassController {
|
export class AdminController implements ClassController {
|
||||||
@@ -25,8 +27,12 @@ export class AdminController implements ClassController {
|
|||||||
return this.app.modules.ctx();
|
return this.app.modules.ctx();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get basepath() {
|
||||||
|
return this.options.basepath ?? "/";
|
||||||
|
}
|
||||||
|
|
||||||
private withBasePath(route: string = "") {
|
private withBasePath(route: string = "") {
|
||||||
return (this.app.modules.configs().server.admin.basepath + route).replace(/\/+$/, "/");
|
return (this.basepath + route).replace(/\/+$/, "/");
|
||||||
}
|
}
|
||||||
|
|
||||||
getController(): Hono<any> {
|
getController(): Hono<any> {
|
||||||
@@ -102,7 +108,10 @@ export class AdminController implements ClassController {
|
|||||||
|
|
||||||
if (this.options.html) {
|
if (this.options.html) {
|
||||||
if (this.options.html.includes(htmlBkndContextReplace)) {
|
if (this.options.html.includes(htmlBkndContextReplace)) {
|
||||||
return this.options.html.replace(htmlBkndContextReplace, bknd_context);
|
return this.options.html.replace(
|
||||||
|
htmlBkndContextReplace,
|
||||||
|
"<script>" + bknd_context + "</script>"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn(
|
console.warn(
|
||||||
@@ -113,6 +122,10 @@ export class AdminController implements ClassController {
|
|||||||
|
|
||||||
const configs = this.app.modules.configs();
|
const configs = this.app.modules.configs();
|
||||||
const isProd = !isDebug() && !this.options.forceDev;
|
const isProd = !isDebug() && !this.options.forceDev;
|
||||||
|
const mainPath =
|
||||||
|
typeof this.options.forceDev === "object" && "mainPath" in this.options.forceDev
|
||||||
|
? this.options.forceDev.mainPath
|
||||||
|
: "/src/ui/main.tsx";
|
||||||
|
|
||||||
const assets = {
|
const assets = {
|
||||||
js: "main.js",
|
js: "main.js",
|
||||||
@@ -166,13 +179,14 @@ export class AdminController implements ClassController {
|
|||||||
)}
|
)}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div id="root" />
|
||||||
<div id="app" />
|
<div id="app" />
|
||||||
<script
|
<script
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: bknd_context
|
__html: bknd_context
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{!isProd && <script type="module" src="/src/ui/main.tsx" />}
|
{!isProd && <script type="module" src={mainPath} />}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|||||||
@@ -74,6 +74,21 @@ export class AppServer extends Module<typeof serverConfigSchema> {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// add an initial fallback route
|
||||||
|
this.client.use("/", async (c, next) => {
|
||||||
|
await next();
|
||||||
|
// if not finalized or giving a 404
|
||||||
|
if (!c.finalized || c.res.status === 404) {
|
||||||
|
// double check it's root
|
||||||
|
if (new URL(c.req.url).pathname === "/") {
|
||||||
|
c.res = undefined;
|
||||||
|
c.res = Response.json({
|
||||||
|
bknd: "hello world!"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.client.onError((err, c) => {
|
this.client.onError((err, c) => {
|
||||||
//throw err;
|
//throw err;
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -82,21 +97,6 @@ export class AppServer extends Module<typeof serverConfigSchema> {
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*if (isDebug()) {
|
|
||||||
console.log("accept", c.req.header("Accept"));
|
|
||||||
if (c.req.header("Accept") === "application/json") {
|
|
||||||
const stack = err.stack;
|
|
||||||
|
|
||||||
if ("toJSON" in err && typeof err.toJSON === "function") {
|
|
||||||
return c.json({ ...err.toJSON(), stack }, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.json({ message: String(err), stack }, 500);
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
|
|
||||||
if (err instanceof Exception) {
|
if (err instanceof Exception) {
|
||||||
console.log("---is exception", err.code);
|
console.log("---is exception", err.code);
|
||||||
return c.json(err.toJSON(), err.code as any);
|
return c.json(err.toJSON(), err.code as any);
|
||||||
@@ -107,32 +107,6 @@ export class AppServer extends Module<typeof serverConfigSchema> {
|
|||||||
this.setBuilt();
|
this.setBuilt();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*setAdminHtml(html: string) {
|
|
||||||
this.admin_html = html;
|
|
||||||
const basepath = (String(this.config.admin.basepath) + "/").replace(/\/+$/, "/");
|
|
||||||
|
|
||||||
const allowed_prefix = basepath + "auth";
|
|
||||||
const login_path = basepath + "auth/login";
|
|
||||||
|
|
||||||
this.client.get(basepath + "*", async (c, next) => {
|
|
||||||
const path = new URL(c.req.url).pathname;
|
|
||||||
if (!path.startsWith(allowed_prefix)) {
|
|
||||||
console.log("guard check permissions");
|
|
||||||
try {
|
|
||||||
this.ctx.guard.throwUnlessGranted(SystemPermissions.admin);
|
|
||||||
} catch (e) {
|
|
||||||
return c.redirect(login_path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.html(this.admin_html!);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getAdminHtml() {
|
|
||||||
return this.admin_html;
|
|
||||||
}*/
|
|
||||||
|
|
||||||
override toJSON(secrets?: boolean) {
|
override toJSON(secrets?: boolean) {
|
||||||
return this.config;
|
return this.config;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,32 @@
|
|||||||
/// <reference types="@cloudflare/workers-types" />
|
/// <reference types="@cloudflare/workers-types" />
|
||||||
|
|
||||||
|
import type { App } from "App";
|
||||||
import type { ClassController } from "core";
|
import type { ClassController } from "core";
|
||||||
import { tbValidator as tb } from "core";
|
import { tbValidator as tb } from "core";
|
||||||
import { StringEnum, Type, TypeInvalidError } from "core/utils";
|
import { StringEnum, Type, TypeInvalidError } from "core/utils";
|
||||||
import { type Context, Hono } from "hono";
|
import { type Context, Hono } from "hono";
|
||||||
import { MODULE_NAMES, type ModuleKey, getDefaultConfig } from "modules/ModuleManager";
|
import {
|
||||||
|
MODULE_NAMES,
|
||||||
|
type ModuleConfigs,
|
||||||
|
type ModuleKey,
|
||||||
|
getDefaultConfig
|
||||||
|
} from "modules/ModuleManager";
|
||||||
import * as SystemPermissions from "modules/permissions";
|
import * as SystemPermissions from "modules/permissions";
|
||||||
import { generateOpenAPI } from "modules/server/openapi";
|
import { generateOpenAPI } from "modules/server/openapi";
|
||||||
import type { App } from "../../App";
|
|
||||||
|
|
||||||
const booleanLike = Type.Transform(Type.String())
|
const booleanLike = Type.Transform(Type.String())
|
||||||
.Decode((v) => v === "1")
|
.Decode((v) => v === "1")
|
||||||
.Encode((v) => (v ? "1" : "0"));
|
.Encode((v) => (v ? "1" : "0"));
|
||||||
|
|
||||||
|
export type ConfigUpdate<Key extends ModuleKey = ModuleKey> = {
|
||||||
|
success: true;
|
||||||
|
module: Key;
|
||||||
|
config: ModuleConfigs[Key];
|
||||||
|
};
|
||||||
|
export type ConfigUpdateResponse<Key extends ModuleKey = ModuleKey> =
|
||||||
|
| ConfigUpdate<Key>
|
||||||
|
| { success: false; type: "type-invalid" | "error" | "unknown"; error?: any; errors?: any };
|
||||||
|
|
||||||
export class SystemController implements ClassController {
|
export class SystemController implements ClassController {
|
||||||
constructor(private readonly app: App) {}
|
constructor(private readonly app: App) {}
|
||||||
|
|
||||||
@@ -60,7 +74,7 @@ export class SystemController implements ClassController {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
async function handleConfigUpdateResponse(c: Context<any>, cb: () => Promise<object>) {
|
async function handleConfigUpdateResponse(c: Context<any>, cb: () => Promise<ConfigUpdate>) {
|
||||||
try {
|
try {
|
||||||
return c.json(await cb(), { status: 202 });
|
return c.json(await cb(), { status: 202 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
+57
-6
@@ -1,26 +1,39 @@
|
|||||||
import { MantineProvider } from "@mantine/core";
|
import { MantineProvider } from "@mantine/core";
|
||||||
import { Notifications } from "@mantine/notifications";
|
import { Notifications } from "@mantine/notifications";
|
||||||
|
import type { ModuleConfigs } from "modules";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { BkndProvider, useBknd } from "ui/client/bknd";
|
||||||
|
import { Logo } from "ui/components/display/Logo";
|
||||||
|
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||||
import { FlashMessage } from "ui/modules/server/FlashMessage";
|
import { FlashMessage } from "ui/modules/server/FlashMessage";
|
||||||
import { BkndProvider, ClientProvider, useBknd } from "./client";
|
import { ClientProvider, type ClientProviderProps } from "./client";
|
||||||
import { createMantineTheme } from "./lib/mantine/theme";
|
import { createMantineTheme } from "./lib/mantine/theme";
|
||||||
import { BkndModalsProvider } from "./modals";
|
import { BkndModalsProvider } from "./modals";
|
||||||
import { Routes } from "./routes";
|
import { Routes } from "./routes";
|
||||||
|
|
||||||
export type BkndAdminProps = {
|
export type BkndAdminProps = {
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
withProvider?: boolean;
|
withProvider?: boolean | ClientProviderProps;
|
||||||
// @todo: add admin config override
|
config?: ModuleConfigs["server"]["admin"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Admin({ baseUrl: baseUrlOverride, withProvider = false }: BkndAdminProps) {
|
export default function Admin({
|
||||||
|
baseUrl: baseUrlOverride,
|
||||||
|
withProvider = false,
|
||||||
|
config
|
||||||
|
}: BkndAdminProps) {
|
||||||
const Component = (
|
const Component = (
|
||||||
<BkndProvider>
|
<BkndProvider adminOverride={config} fallback={<Skeleton theme={config?.color_scheme} />}>
|
||||||
<AdminInternal />
|
<AdminInternal />
|
||||||
</BkndProvider>
|
</BkndProvider>
|
||||||
);
|
);
|
||||||
return withProvider ? (
|
return withProvider ? (
|
||||||
<ClientProvider baseUrl={baseUrlOverride}>{Component}</ClientProvider>
|
<ClientProvider
|
||||||
|
baseUrl={baseUrlOverride}
|
||||||
|
{...(typeof withProvider === "object" ? withProvider : {})}
|
||||||
|
>
|
||||||
|
{Component}
|
||||||
|
</ClientProvider>
|
||||||
) : (
|
) : (
|
||||||
Component
|
Component
|
||||||
);
|
);
|
||||||
@@ -40,3 +53,41 @@ function AdminInternal() {
|
|||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Skeleton = ({ theme = "light" }: { theme?: string }) => {
|
||||||
|
return (
|
||||||
|
<div id="bknd-admin" className={(theme ?? "light") + " antialiased"}>
|
||||||
|
<AppShell.Root>
|
||||||
|
<header
|
||||||
|
data-shell="header"
|
||||||
|
className="flex flex-row w-full h-16 gap-2.5 border-muted border-b justify-start bg-muted/10"
|
||||||
|
>
|
||||||
|
<div className="max-h-full flex hover:bg-primary/5 link p-2.5 w-[134px] outline-none">
|
||||||
|
<Logo theme={theme} />
|
||||||
|
</div>
|
||||||
|
<nav className="hidden md:flex flex-row gap-2.5 pl-0 p-2.5 items-center">
|
||||||
|
{[...new Array(5)].map((item, key) => (
|
||||||
|
<AppShell.NavLink key={key} as="span" className="active h-full opacity-50">
|
||||||
|
<div className="w-10 h-3" />
|
||||||
|
</AppShell.NavLink>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
<nav className="flex md:hidden flex-row items-center">
|
||||||
|
<AppShell.NavLink as="span" className="active h-full opacity-50">
|
||||||
|
<div className="w-10 h-3" />
|
||||||
|
</AppShell.NavLink>
|
||||||
|
</nav>
|
||||||
|
<div className="flex flex-grow" />
|
||||||
|
<div className="hidden lg:flex flex-row items-center px-4 gap-2 opacity-50">
|
||||||
|
<div className="size-11 rounded-full bg-primary/10" />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<AppShell.Content>
|
||||||
|
<div className="flex flex-col w-full h-full justify-center items-center">
|
||||||
|
<span className="font-mono opacity-30">Loading</span>
|
||||||
|
</div>
|
||||||
|
</AppShell.Content>
|
||||||
|
</AppShell.Root>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
//import { notifications } from "@mantine/notifications";
|
import type { ModuleConfigs, ModuleSchemas } from "modules";
|
||||||
import { getDefaultConfig, getDefaultSchema } from "modules/ModuleManager";
|
import { getDefaultConfig, getDefaultSchema } from "modules/ModuleManager";
|
||||||
import { createContext, startTransition, useContext, useEffect, useRef, useState } from "react";
|
import { createContext, startTransition, useContext, useEffect, useRef, useState } from "react";
|
||||||
import type { ModuleConfigs, ModuleSchemas } from "../../modules";
|
import { useApi } from "ui/client";
|
||||||
import { useClient } from "./ClientProvider";
|
|
||||||
import { type TSchemaActions, getSchemaActions } from "./schema/actions";
|
import { type TSchemaActions, getSchemaActions } from "./schema/actions";
|
||||||
import { AppReduced } from "./utils/AppReduced";
|
import { AppReduced } from "./utils/AppReduced";
|
||||||
|
|
||||||
@@ -14,6 +13,7 @@ type BkndContext = {
|
|||||||
requireSecrets: () => Promise<void>;
|
requireSecrets: () => Promise<void>;
|
||||||
actions: ReturnType<typeof getSchemaActions>;
|
actions: ReturnType<typeof getSchemaActions>;
|
||||||
app: AppReduced;
|
app: AppReduced;
|
||||||
|
adminOverride?: ModuleConfigs["server"]["admin"];
|
||||||
};
|
};
|
||||||
|
|
||||||
const BkndContext = createContext<BkndContext>(undefined!);
|
const BkndContext = createContext<BkndContext>(undefined!);
|
||||||
@@ -21,14 +21,19 @@ export type { TSchemaActions };
|
|||||||
|
|
||||||
export function BkndProvider({
|
export function BkndProvider({
|
||||||
includeSecrets = false,
|
includeSecrets = false,
|
||||||
children
|
adminOverride,
|
||||||
}: { includeSecrets?: boolean; children: any }) {
|
children,
|
||||||
|
fallback = null
|
||||||
|
}: { includeSecrets?: boolean; children: any; fallback?: React.ReactNode } & Pick<
|
||||||
|
BkndContext,
|
||||||
|
"adminOverride"
|
||||||
|
>) {
|
||||||
const [withSecrets, setWithSecrets] = useState<boolean>(includeSecrets);
|
const [withSecrets, setWithSecrets] = useState<boolean>(includeSecrets);
|
||||||
const [schema, setSchema] =
|
const [schema, setSchema] =
|
||||||
useState<Pick<BkndContext, "version" | "schema" | "config" | "permissions">>();
|
useState<Pick<BkndContext, "version" | "schema" | "config" | "permissions">>();
|
||||||
const [fetched, setFetched] = useState(false);
|
const [fetched, setFetched] = useState(false);
|
||||||
const errorShown = useRef<boolean>();
|
const errorShown = useRef<boolean>();
|
||||||
const client = useClient();
|
const api = useApi();
|
||||||
|
|
||||||
async function reloadSchema() {
|
async function reloadSchema() {
|
||||||
await fetchSchema(includeSecrets, true);
|
await fetchSchema(includeSecrets, true);
|
||||||
@@ -36,7 +41,7 @@ export function BkndProvider({
|
|||||||
|
|
||||||
async function fetchSchema(_includeSecrets: boolean = false, force?: boolean) {
|
async function fetchSchema(_includeSecrets: boolean = false, force?: boolean) {
|
||||||
if (withSecrets && !force) return;
|
if (withSecrets && !force) return;
|
||||||
const { body, res } = await client.api.system.readSchema({
|
const res = await api.system.readSchema({
|
||||||
config: true,
|
config: true,
|
||||||
secrets: _includeSecrets
|
secrets: _includeSecrets
|
||||||
});
|
});
|
||||||
@@ -56,7 +61,7 @@ export function BkndProvider({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const schema = res.ok
|
const schema = res.ok
|
||||||
? body
|
? res.body
|
||||||
: ({
|
: ({
|
||||||
version: 0,
|
version: 0,
|
||||||
schema: getDefaultSchema(),
|
schema: getDefaultSchema(),
|
||||||
@@ -64,6 +69,13 @@ export function BkndProvider({
|
|||||||
permissions: []
|
permissions: []
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
|
if (adminOverride) {
|
||||||
|
schema.config.server.admin = {
|
||||||
|
...schema.config.server.admin,
|
||||||
|
...adminOverride
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
setSchema(schema);
|
setSchema(schema);
|
||||||
setWithSecrets(_includeSecrets);
|
setWithSecrets(_includeSecrets);
|
||||||
@@ -81,31 +93,17 @@ export function BkndProvider({
|
|||||||
fetchSchema(includeSecrets);
|
fetchSchema(includeSecrets);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!fetched || !schema) return null;
|
if (!fetched || !schema) return fallback;
|
||||||
const app = new AppReduced(schema?.config as any);
|
const app = new AppReduced(schema?.config as any);
|
||||||
const actions = getSchemaActions({ client, setSchema, reloadSchema });
|
const actions = getSchemaActions({ api, setSchema, reloadSchema });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BkndContext.Provider value={{ ...schema, actions, requireSecrets, app }}>
|
<BkndContext.Provider value={{ ...schema, actions, requireSecrets, app, adminOverride }}>
|
||||||
{children}
|
{children}
|
||||||
</BkndContext.Provider>
|
</BkndContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type BkndWindowContext = {
|
|
||||||
user?: object;
|
|
||||||
logout_route: string;
|
|
||||||
};
|
|
||||||
export function useBkndWindowContext(): BkndWindowContext {
|
|
||||||
if (typeof window !== "undefined" && window.__BKND__) {
|
|
||||||
return window.__BKND__ as any;
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
logout_route: "/api/auth/logout"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useBknd({ withSecrets }: { withSecrets?: boolean } = {}): BkndContext {
|
export function useBknd({ withSecrets }: { withSecrets?: boolean } = {}): BkndContext {
|
||||||
const ctx = useContext(BkndContext);
|
const ctx = useContext(BkndContext);
|
||||||
if (withSecrets) ctx.requireSecrets();
|
if (withSecrets) ctx.requireSecrets();
|
||||||
|
|||||||
@@ -1,89 +1,72 @@
|
|||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { Api, type ApiOptions, type TApiUser } from "Api";
|
||||||
import type { TApiUser } from "Api";
|
|
||||||
import { createContext, useContext, useEffect, useState } from "react";
|
import { createContext, useContext, useEffect, useState } from "react";
|
||||||
import { useBkndWindowContext } from "ui/client/BkndProvider";
|
|
||||||
import { AppQueryClient } from "./utils/AppQueryClient";
|
|
||||||
|
|
||||||
const ClientContext = createContext<{ baseUrl: string; client: AppQueryClient }>({
|
const ClientContext = createContext<{ baseUrl: string; api: Api }>({
|
||||||
baseUrl: undefined
|
baseUrl: undefined
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
export const queryClient = new QueryClient({
|
export type ClientProviderProps = {
|
||||||
defaultOptions: {
|
children?: any;
|
||||||
queries: {
|
baseUrl?: string;
|
||||||
retry: false,
|
user?: TApiUser | null | undefined;
|
||||||
refetchOnWindowFocus: false
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ClientProvider = ({
|
export const ClientProvider = ({ children, baseUrl, user }: ClientProviderProps) => {
|
||||||
children,
|
//const [actualBaseUrl, setActualBaseUrl] = useState<string | null>(null);
|
||||||
baseUrl,
|
|
||||||
user
|
|
||||||
}: { children?: any; baseUrl?: string; user?: TApiUser | null }) => {
|
|
||||||
const [actualBaseUrl, setActualBaseUrl] = useState<string | null>(null);
|
|
||||||
const winCtx = useBkndWindowContext();
|
const winCtx = useBkndWindowContext();
|
||||||
|
const _ctx_baseUrl = useBaseUrl();
|
||||||
|
let actualBaseUrl = baseUrl ?? _ctx_baseUrl ?? "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const _ctx_baseUrl = useBaseUrl();
|
if (!baseUrl) {
|
||||||
if (_ctx_baseUrl) {
|
if (_ctx_baseUrl) {
|
||||||
console.warn("wrapped many times");
|
actualBaseUrl = _ctx_baseUrl;
|
||||||
setActualBaseUrl(_ctx_baseUrl);
|
console.warn("wrapped many times, take from context", actualBaseUrl);
|
||||||
|
} else if (typeof window !== "undefined") {
|
||||||
|
actualBaseUrl = window.location.origin;
|
||||||
|
console.log("setting from window", actualBaseUrl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("error", e);
|
console.error("error .....", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
const api = new Api({ host: actualBaseUrl, user: user ?? winCtx.user });
|
||||||
// Only set base URL if running on the client side
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
setActualBaseUrl(baseUrl || window.location.origin);
|
|
||||||
}
|
|
||||||
}, [baseUrl]);
|
|
||||||
|
|
||||||
if (!actualBaseUrl) {
|
|
||||||
// Optionally, return a fallback during SSR rendering
|
|
||||||
return null; // or a loader/spinner if desired
|
|
||||||
}
|
|
||||||
|
|
||||||
//console.log("client provider11 with", { baseUrl, fallback: actualBaseUrl, user });
|
|
||||||
const client = createClient(actualBaseUrl, user ?? winCtx.user);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<ClientContext.Provider value={{ baseUrl: api.baseUrl, api }}>
|
||||||
<ClientContext.Provider value={{ baseUrl: actualBaseUrl, client }}>
|
{children}
|
||||||
{children}
|
</ClientContext.Provider>
|
||||||
</ClientContext.Provider>
|
|
||||||
</QueryClientProvider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createClient(baseUrl: string, user?: object) {
|
export const useApi = (host?: ApiOptions["host"]): Api => {
|
||||||
return new AppQueryClient(baseUrl, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createOrUseClient(baseUrl: string) {
|
|
||||||
const context = useContext(ClientContext);
|
const context = useContext(ClientContext);
|
||||||
if (!context) {
|
if (!context?.api || (host && host.length > 0 && host !== context.baseUrl)) {
|
||||||
console.warn("createOrUseClient returned a new client");
|
return new Api({ host: host ?? "" });
|
||||||
return createClient(baseUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return context.client;
|
return context.api;
|
||||||
}
|
|
||||||
|
|
||||||
export const useClient = () => {
|
|
||||||
const context = useContext(ClientContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useClient must be used within a ClientProvider");
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("useClient", context.baseUrl);
|
|
||||||
return context.client;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use useApi().baseUrl instead
|
||||||
|
*/
|
||||||
export const useBaseUrl = () => {
|
export const useBaseUrl = () => {
|
||||||
const context = useContext(ClientContext);
|
const context = useContext(ClientContext);
|
||||||
return context.baseUrl;
|
return context.baseUrl;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type BkndWindowContext = {
|
||||||
|
user?: TApiUser;
|
||||||
|
logout_route: string;
|
||||||
|
};
|
||||||
|
export function useBkndWindowContext(): BkndWindowContext {
|
||||||
|
if (typeof window !== "undefined" && window.__BKND__) {
|
||||||
|
return window.__BKND__ as any;
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
logout_route: "/api/auth/logout"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import type { Api } from "Api";
|
||||||
|
import type { FetchPromise, ResponseObject } from "modules/ModuleApi";
|
||||||
|
import useSWR, { type SWRConfiguration, useSWRConfig } from "swr";
|
||||||
|
import { useApi } from "ui/client";
|
||||||
|
|
||||||
|
export const useApiQuery = <
|
||||||
|
Data,
|
||||||
|
RefineFn extends (data: ResponseObject<Data>) => unknown = (data: ResponseObject<Data>) => Data
|
||||||
|
>(
|
||||||
|
fn: (api: Api) => FetchPromise<Data>,
|
||||||
|
options?: SWRConfiguration & { enabled?: boolean; refine?: RefineFn }
|
||||||
|
) => {
|
||||||
|
const api = useApi();
|
||||||
|
const promise = fn(api);
|
||||||
|
const refine = options?.refine ?? ((data: any) => data);
|
||||||
|
const fetcher = () => promise.execute().then(refine);
|
||||||
|
const key = promise.key();
|
||||||
|
|
||||||
|
type RefinedData = RefineFn extends (data: ResponseObject<Data>) => infer R ? R : Data;
|
||||||
|
|
||||||
|
const swr = useSWR<RefinedData>(options?.enabled === false ? null : key, fetcher, options);
|
||||||
|
return {
|
||||||
|
...swr,
|
||||||
|
promise,
|
||||||
|
key,
|
||||||
|
api
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useInvalidate = () => {
|
||||||
|
const mutate = useSWRConfig().mutate;
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
return async (arg?: string | ((api: Api) => FetchPromise<any>)) => {
|
||||||
|
if (!arg) return async () => mutate("");
|
||||||
|
return mutate(typeof arg === "string" ? arg : arg(api).key());
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
import type { DB, PrimaryFieldType } from "core";
|
||||||
|
import { encodeSearch, objectTransform } from "core/utils";
|
||||||
|
import type { EntityData, RepoQuery } from "data";
|
||||||
|
import type { ModuleApi, ResponseObject } from "modules/ModuleApi";
|
||||||
|
import useSWR, { type SWRConfiguration, mutate } from "swr";
|
||||||
|
import { type Api, useApi } from "ui/client";
|
||||||
|
|
||||||
|
export class UseEntityApiError<Payload = any> extends Error {
|
||||||
|
constructor(
|
||||||
|
public response: ResponseObject<Payload>,
|
||||||
|
fallback?: string
|
||||||
|
) {
|
||||||
|
let message = fallback;
|
||||||
|
if ("error" in response) {
|
||||||
|
message = response.error as string;
|
||||||
|
if (fallback) {
|
||||||
|
message = `${fallback}: ${message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
super(message ?? "UseEntityApiError");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test() {
|
||||||
|
const { read } = useEntity("users");
|
||||||
|
async () => {
|
||||||
|
const data = await read();
|
||||||
|
};
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useEntity = <
|
||||||
|
Entity extends keyof DB | string,
|
||||||
|
Id extends PrimaryFieldType | undefined = undefined,
|
||||||
|
Data = Entity extends keyof DB ? DB[Entity] : EntityData
|
||||||
|
>(
|
||||||
|
entity: Entity,
|
||||||
|
id?: Id
|
||||||
|
) => {
|
||||||
|
const api = useApi().data;
|
||||||
|
|
||||||
|
return {
|
||||||
|
create: async (input: Omit<Data, "id">) => {
|
||||||
|
const res = await api.createOne(entity, input);
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new UseEntityApiError(res, `Failed to create entity "${entity}"`);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
read: async (query: Partial<RepoQuery> = {}) => {
|
||||||
|
const res = id ? await api.readOne(entity, id!, query) : await api.readMany(entity, query);
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new UseEntityApiError(res as any, `Failed to read entity "${entity}"`);
|
||||||
|
}
|
||||||
|
// must be manually typed
|
||||||
|
return res as unknown as Id extends undefined
|
||||||
|
? ResponseObject<Data[]>
|
||||||
|
: ResponseObject<Data>;
|
||||||
|
},
|
||||||
|
update: async (input: Partial<Omit<Data, "id">>, _id: PrimaryFieldType | undefined = id) => {
|
||||||
|
if (!_id) {
|
||||||
|
throw new Error("id is required");
|
||||||
|
}
|
||||||
|
const res = await api.updateOne(entity, _id, input);
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new UseEntityApiError(res, `Failed to update entity "${entity}"`);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
_delete: async (_id: PrimaryFieldType | undefined = id) => {
|
||||||
|
if (!_id) {
|
||||||
|
throw new Error("id is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await api.deleteOne(entity, _id);
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new UseEntityApiError(res, `Failed to delete entity "${entity}"`);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// @todo: try to get from ModuleApi directly
|
||||||
|
export function makeKey(
|
||||||
|
api: ModuleApi,
|
||||||
|
entity: string,
|
||||||
|
id?: PrimaryFieldType,
|
||||||
|
query?: Partial<RepoQuery>
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
"/" +
|
||||||
|
[...(api.options?.basepath?.split("/") ?? []), entity, ...(id ? [id] : [])]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("/") +
|
||||||
|
(query ? "?" + encodeSearch(query) : "")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useEntityQuery = <
|
||||||
|
Entity extends keyof DB | string,
|
||||||
|
Id extends PrimaryFieldType | undefined = undefined
|
||||||
|
>(
|
||||||
|
entity: Entity,
|
||||||
|
id?: Id,
|
||||||
|
query?: Partial<RepoQuery>,
|
||||||
|
options?: SWRConfiguration & { enabled?: boolean; revalidateOnMutate?: boolean }
|
||||||
|
) => {
|
||||||
|
const api = useApi().data;
|
||||||
|
const key = makeKey(api, entity, id, query);
|
||||||
|
const { read, ...actions } = useEntity<Entity, Id>(entity, id);
|
||||||
|
const fetcher = () => read(query);
|
||||||
|
|
||||||
|
type T = Awaited<ReturnType<typeof fetcher>>;
|
||||||
|
const swr = useSWR<T>(options?.enabled === false ? null : key, fetcher as any, {
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
keepPreviousData: true,
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
|
||||||
|
const mutateAll = async () => {
|
||||||
|
const entityKey = makeKey(api, entity);
|
||||||
|
return mutate((key) => typeof key === "string" && key.startsWith(entityKey), undefined, {
|
||||||
|
revalidate: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapped = objectTransform(actions, (action) => {
|
||||||
|
return async (...args: any) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const res = await action(...args);
|
||||||
|
|
||||||
|
// mutate all keys of entity by default
|
||||||
|
if (options?.revalidateOnMutate !== false) {
|
||||||
|
await mutateAll();
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
}) as Omit<ReturnType<typeof useEntity<Entity, Id>>, "read">;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...swr,
|
||||||
|
...mapped,
|
||||||
|
api,
|
||||||
|
key
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function mutateEntityCache<
|
||||||
|
Entity extends keyof DB | string,
|
||||||
|
Data = Entity extends keyof DB ? Omit<DB[Entity], "id"> : EntityData
|
||||||
|
>(api: Api["data"], entity: Entity, id: PrimaryFieldType, partialData: Partial<Data>) {
|
||||||
|
function update(prev: any, partialNext: any) {
|
||||||
|
if (
|
||||||
|
typeof prev !== "undefined" &&
|
||||||
|
typeof partialNext !== "undefined" &&
|
||||||
|
"id" in prev &&
|
||||||
|
prev.id === id
|
||||||
|
) {
|
||||||
|
return { ...prev, ...partialNext };
|
||||||
|
}
|
||||||
|
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entityKey = makeKey(api, entity);
|
||||||
|
|
||||||
|
return mutate(
|
||||||
|
(key) => typeof key === "string" && key.startsWith(entityKey),
|
||||||
|
async (data) => {
|
||||||
|
if (typeof data === "undefined") return;
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
return data.map((item) => update(item, partialData));
|
||||||
|
}
|
||||||
|
return update(data, partialData);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
revalidate: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useEntityMutate = <
|
||||||
|
Entity extends keyof DB | string,
|
||||||
|
Id extends PrimaryFieldType | undefined = undefined,
|
||||||
|
Data = Entity extends keyof DB ? Omit<DB[Entity], "id"> : EntityData
|
||||||
|
>(
|
||||||
|
entity: Entity,
|
||||||
|
id?: Id,
|
||||||
|
options?: SWRConfiguration
|
||||||
|
) => {
|
||||||
|
const { data, ...$q } = useEntityQuery<Entity, Id>(entity, id, undefined, {
|
||||||
|
...options,
|
||||||
|
enabled: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const _mutate = id
|
||||||
|
? (data) => mutateEntityCache($q.api, entity, id, data)
|
||||||
|
: (id, data) => mutateEntityCache($q.api, entity, id, data);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...$q,
|
||||||
|
mutate: _mutate as unknown as Id extends undefined
|
||||||
|
? (id: PrimaryFieldType, data: Partial<Data>) => Promise<void>
|
||||||
|
: (data: Partial<Data>) => Promise<void>
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { BkndProvider, useBknd } from "./BkndProvider";
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
export { ClientProvider, useClient, useBaseUrl } from "./ClientProvider";
|
export {
|
||||||
export { BkndProvider, useBknd } from "./BkndProvider";
|
ClientProvider,
|
||||||
|
useBkndWindowContext,
|
||||||
|
type ClientProviderProps,
|
||||||
|
useApi,
|
||||||
|
useBaseUrl
|
||||||
|
} from "./ClientProvider";
|
||||||
|
|
||||||
|
export * from "./api/use-api";
|
||||||
|
export * from "./api/use-entity";
|
||||||
export { useAuth } from "./schema/auth/use-auth";
|
export { useAuth } from "./schema/auth/use-auth";
|
||||||
export { Api } from "../../Api";
|
export { Api } from "../../Api";
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
import { type NotificationData, notifications } from "@mantine/notifications";
|
import { type NotificationData, notifications } from "@mantine/notifications";
|
||||||
|
import type { Api } from "Api";
|
||||||
import { ucFirst } from "core/utils";
|
import { ucFirst } from "core/utils";
|
||||||
import type { ApiResponse, ModuleConfigs } from "../../../modules";
|
import type { ModuleConfigs } from "modules";
|
||||||
import type { AppQueryClient } from "../utils/AppQueryClient";
|
import type { ResponseObject } from "modules/ModuleApi";
|
||||||
|
import type { ConfigUpdateResponse } from "modules/server/SystemController";
|
||||||
|
|
||||||
export type SchemaActionsProps = {
|
export type SchemaActionsProps = {
|
||||||
client: AppQueryClient;
|
api: Api;
|
||||||
setSchema: React.Dispatch<React.SetStateAction<any>>;
|
setSchema: React.Dispatch<React.SetStateAction<any>>;
|
||||||
reloadSchema: () => Promise<void>;
|
reloadSchema: () => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSchemaActions = ReturnType<typeof getSchemaActions>;
|
export type TSchemaActions = ReturnType<typeof getSchemaActions>;
|
||||||
|
|
||||||
export function getSchemaActions({ client, setSchema, reloadSchema }: SchemaActionsProps) {
|
export function getSchemaActions({ api, setSchema, reloadSchema }: SchemaActionsProps) {
|
||||||
const api = client.api;
|
async function handleConfigUpdate<Module extends keyof ModuleConfigs>(
|
||||||
|
|
||||||
async function handleConfigUpdate(
|
|
||||||
action: string,
|
action: string,
|
||||||
module: string,
|
module: Module,
|
||||||
res: ApiResponse,
|
res: ResponseObject<ConfigUpdateResponse<Module>>,
|
||||||
path?: string
|
path?: string
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const base: Partial<NotificationData> = {
|
const base: Partial<NotificationData> = {
|
||||||
@@ -26,7 +26,7 @@ export function getSchemaActions({ client, setSchema, reloadSchema }: SchemaActi
|
|||||||
autoClose: 3000
|
autoClose: 3000
|
||||||
};
|
};
|
||||||
|
|
||||||
if (res.res.ok && res.body.success) {
|
if (res.success === true) {
|
||||||
console.log("update config", action, module, path, res.body);
|
console.log("update config", action, module, path, res.body);
|
||||||
if (res.body.success) {
|
if (res.body.success) {
|
||||||
setSchema((prev) => {
|
setSchema((prev) => {
|
||||||
@@ -35,7 +35,7 @@ export function getSchemaActions({ client, setSchema, reloadSchema }: SchemaActi
|
|||||||
...prev,
|
...prev,
|
||||||
config: {
|
config: {
|
||||||
...prev.config,
|
...prev.config,
|
||||||
[module]: res.body.config
|
[module]: res.config
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -47,18 +47,18 @@ export function getSchemaActions({ client, setSchema, reloadSchema }: SchemaActi
|
|||||||
color: "blue",
|
color: "blue",
|
||||||
message: `Operation ${action.toUpperCase()} at ${module}${path ? "." + path : ""}`
|
message: `Operation ${action.toUpperCase()} at ${module}${path ? "." + path : ""}`
|
||||||
});
|
});
|
||||||
return true;
|
} else {
|
||||||
|
notifications.show({
|
||||||
|
...base,
|
||||||
|
title: `Config Update failed: ${ucFirst(module)}${path ? "." + path : ""}`,
|
||||||
|
color: "red",
|
||||||
|
withCloseButton: true,
|
||||||
|
autoClose: false,
|
||||||
|
message: res.error ?? "Failed to complete config update"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
notifications.show({
|
return res.success;
|
||||||
...base,
|
|
||||||
title: `Config Update failed: ${ucFirst(module)}${path ? "." + path : ""}`,
|
|
||||||
color: "red",
|
|
||||||
withCloseButton: true,
|
|
||||||
autoClose: false,
|
|
||||||
message: res.body.error ?? "Failed to complete config update"
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -72,7 +72,7 @@ export function getSchemaActions({ client, setSchema, reloadSchema }: SchemaActi
|
|||||||
return await handleConfigUpdate("set", module, res);
|
return await handleConfigUpdate("set", module, res);
|
||||||
},
|
},
|
||||||
patch: async <Module extends keyof ModuleConfigs>(
|
patch: async <Module extends keyof ModuleConfigs>(
|
||||||
module: keyof ModuleConfigs,
|
module: Module,
|
||||||
path: string,
|
path: string,
|
||||||
value: any
|
value: any
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
@@ -80,25 +80,18 @@ export function getSchemaActions({ client, setSchema, reloadSchema }: SchemaActi
|
|||||||
return await handleConfigUpdate("patch", module, res, path);
|
return await handleConfigUpdate("patch", module, res, path);
|
||||||
},
|
},
|
||||||
overwrite: async <Module extends keyof ModuleConfigs>(
|
overwrite: async <Module extends keyof ModuleConfigs>(
|
||||||
module: keyof ModuleConfigs,
|
module: Module,
|
||||||
path: string,
|
path: string,
|
||||||
value: any
|
value: any
|
||||||
) => {
|
) => {
|
||||||
const res = await api.system.overwriteConfig(module, path, value);
|
const res = await api.system.overwriteConfig(module, path, value);
|
||||||
return await handleConfigUpdate("overwrite", module, res, path);
|
return await handleConfigUpdate("overwrite", module, res, path);
|
||||||
},
|
},
|
||||||
add: async <Module extends keyof ModuleConfigs>(
|
add: async <Module extends keyof ModuleConfigs>(module: Module, path: string, value: any) => {
|
||||||
module: keyof ModuleConfigs,
|
|
||||||
path: string,
|
|
||||||
value: any
|
|
||||||
) => {
|
|
||||||
const res = await api.system.addConfig(module, path, value);
|
const res = await api.system.addConfig(module, path, value);
|
||||||
return await handleConfigUpdate("add", module, res, path);
|
return await handleConfigUpdate("add", module, res, path);
|
||||||
},
|
},
|
||||||
remove: async <Module extends keyof ModuleConfigs>(
|
remove: async <Module extends keyof ModuleConfigs>(module: Module, path: string) => {
|
||||||
module: keyof ModuleConfigs,
|
|
||||||
path: string
|
|
||||||
) => {
|
|
||||||
const res = await api.system.removeConfig(module, path);
|
const res = await api.system.removeConfig(module, path);
|
||||||
return await handleConfigUpdate("remove", module, res, path);
|
return await handleConfigUpdate("remove", module, res, path);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
import { Api } from "Api";
|
import { Api, type AuthState } from "Api";
|
||||||
import type { AuthResponse } from "auth";
|
import type { AuthResponse } from "auth";
|
||||||
import type { AppAuthSchema } from "auth/auth-schema";
|
import type { AppAuthSchema } from "auth/auth-schema";
|
||||||
import type { ApiResponse } from "modules/ModuleApi";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import { useApi, useInvalidate } from "ui/client";
|
||||||
createClient,
|
|
||||||
createOrUseClient,
|
|
||||||
queryClient,
|
|
||||||
useBaseUrl,
|
|
||||||
useClient
|
|
||||||
} from "../../ClientProvider";
|
|
||||||
|
|
||||||
type LoginData = {
|
type LoginData = {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -18,55 +11,54 @@ type LoginData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type UseAuth = {
|
type UseAuth = {
|
||||||
data: (AuthResponse & { verified: boolean }) | undefined;
|
data: AuthState | undefined;
|
||||||
user: AuthResponse["user"] | undefined;
|
user: AuthState["user"] | undefined;
|
||||||
token: AuthResponse["token"] | undefined;
|
token: AuthState["token"] | undefined;
|
||||||
verified: boolean;
|
verified: boolean;
|
||||||
login: (data: LoginData) => Promise<ApiResponse<AuthResponse>>;
|
login: (data: LoginData) => Promise<AuthResponse>;
|
||||||
register: (data: LoginData) => Promise<ApiResponse<AuthResponse>>;
|
register: (data: LoginData) => Promise<AuthResponse>;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
verify: () => void;
|
verify: () => void;
|
||||||
setToken: (token: string) => void;
|
setToken: (token: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
// @todo: needs to use a specific auth endpoint to get strategy information
|
|
||||||
export const useAuth = (options?: { baseUrl?: string }): UseAuth => {
|
export const useAuth = (options?: { baseUrl?: string }): UseAuth => {
|
||||||
const ctxBaseUrl = useBaseUrl();
|
const api = useApi(options?.baseUrl);
|
||||||
//const client = useClient();
|
const invalidate = useInvalidate();
|
||||||
const client = createOrUseClient(options?.baseUrl ? options?.baseUrl : ctxBaseUrl);
|
const authState = api.getAuthState();
|
||||||
const authState = client.auth().state();
|
|
||||||
const [authData, setAuthData] = useState<UseAuth["data"]>(authState);
|
const [authData, setAuthData] = useState<UseAuth["data"]>(authState);
|
||||||
const verified = authState?.verified ?? false;
|
const verified = authState?.verified ?? false;
|
||||||
|
|
||||||
|
function updateAuthState() {
|
||||||
|
setAuthData(api.getAuthState());
|
||||||
|
}
|
||||||
|
|
||||||
async function login(input: LoginData) {
|
async function login(input: LoginData) {
|
||||||
const res = await client.auth().login(input);
|
const res = await api.auth.loginWithPassword(input);
|
||||||
if (res.res.ok && res.data && "user" in res.data) {
|
updateAuthState();
|
||||||
setAuthData(res.data);
|
return res.data;
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function register(input: LoginData) {
|
async function register(input: LoginData) {
|
||||||
const res = await client.auth().register(input);
|
const res = await api.auth.registerWithPassword(input);
|
||||||
if (res.res.ok && res.data && "user" in res.data) {
|
updateAuthState();
|
||||||
setAuthData(res.data);
|
return res.data;
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setToken(token: string) {
|
function setToken(token: string) {
|
||||||
setAuthData(client.auth().setToken(token) as any);
|
api.updateToken(token);
|
||||||
|
updateAuthState();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
await client.auth().logout();
|
await api.updateToken(undefined);
|
||||||
setAuthData(undefined);
|
setAuthData(undefined);
|
||||||
queryClient.clear();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function verify() {
|
async function verify() {
|
||||||
await client.auth().verify();
|
await api.verifyAuth();
|
||||||
setAuthData(client.auth().state());
|
updateAuthState();
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -87,10 +79,7 @@ export const useAuthStrategies = (options?: { baseUrl?: string }): Partial<AuthS
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
} => {
|
} => {
|
||||||
const [data, setData] = useState<AuthStrategyData>();
|
const [data, setData] = useState<AuthStrategyData>();
|
||||||
const ctxBaseUrl = useBaseUrl();
|
const api = useApi(options?.baseUrl);
|
||||||
const api = new Api({
|
|
||||||
host: options?.baseUrl ? options?.baseUrl : ctxBaseUrl
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { useBknd } from "ui/client";
|
import { useBknd } from "ui/client/bknd";
|
||||||
|
|
||||||
export function useBkndAuth() {
|
export function useBkndAuth() {
|
||||||
//const client = useClient();
|
const { config, schema, actions: bkndActions } = useBknd();
|
||||||
const { config, app, schema, actions: bkndActions } = useBknd();
|
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
roles: {
|
roles: {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user