mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 16:16:02 +00:00
Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d2c75b1605 | |||
| a474e3fe3a | |||
| d81b3acb94 | |||
| 2ca66f4fc9 | |||
| a8bbb6e760 | |||
| 2395d7fe97 | |||
| 10d606f8e9 | |||
| 13a88be8d1 | |||
| a5264e22ee | |||
| 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 | |||
| 4693be5615 | |||
| bdc6eb55bf | |||
| 54b38401d8 | |||
| 49c2a7b4db | |||
| 6718419d41 |
+4
-1
@@ -17,10 +17,13 @@ packages/media/.env
|
||||
**/*/vite.config.ts.timestamp*
|
||||
.history
|
||||
**/*/.db/*
|
||||
**/*/*.db
|
||||
**/*/*.db-shm
|
||||
**/*/*.db-wal
|
||||
**/*/.tmp
|
||||
.npmrc
|
||||
/.verdaccio
|
||||
.idea
|
||||
.vscode
|
||||
.git_old
|
||||
.git_old
|
||||
docker/tmp
|
||||
@@ -1,7 +1,8 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { mark, stripMark } from "../src/core/utils";
|
||||
import { ModuleManager } from "../src/modules/ModuleManager";
|
||||
import { CURRENT_VERSION, TABLE_NAME, migrateSchema } from "../src/modules/migrations";
|
||||
import { entity, text } from "../src/data";
|
||||
import { ModuleManager, getDefaultConfig } from "../src/modules/ModuleManager";
|
||||
import { CURRENT_VERSION, TABLE_NAME } from "../src/modules/migrations";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
describe("ModuleManager", async () => {
|
||||
@@ -29,21 +30,68 @@ describe("ModuleManager", async () => {
|
||||
const mm = new ModuleManager(c.dummyConnection);
|
||||
await mm.build();
|
||||
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 c2 = getDummyConnection();
|
||||
const db = c2.dummyConnection.kysely;
|
||||
await migrateSchema(CURRENT_VERSION, { db });
|
||||
const mm2 = new ModuleManager(c2.dummyConnection, { initial: { version, ...json } });
|
||||
await mm2.syncConfigTable();
|
||||
await db
|
||||
.updateTable(TABLE_NAME)
|
||||
.set({ json: JSON.stringify(json), version: CURRENT_VERSION })
|
||||
.insertInto(TABLE_NAME)
|
||||
.values({ type: "config", json: JSON.stringify(json), version: CURRENT_VERSION })
|
||||
.execute();
|
||||
|
||||
const mm2 = new ModuleManager(c2.dummyConnection, { initial: { version, ...json } });
|
||||
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 () => {
|
||||
@@ -52,21 +100,19 @@ describe("ModuleManager", async () => {
|
||||
await mm.build();
|
||||
const version = mm.version();
|
||||
const json = mm.configs();
|
||||
//const { version, ...json } = mm.toJSON() as any;
|
||||
|
||||
const c2 = getDummyConnection();
|
||||
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, {
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -80,15 +126,15 @@ describe("ModuleManager", async () => {
|
||||
|
||||
const c2 = getDummyConnection();
|
||||
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, {
|
||||
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/);
|
||||
});
|
||||
@@ -102,7 +148,9 @@ describe("ModuleManager", async () => {
|
||||
|
||||
const c2 = getDummyConnection();
|
||||
const db = c2.dummyConnection.kysely;
|
||||
await migrateSchema(CURRENT_VERSION, { db });
|
||||
|
||||
const mm2 = new ModuleManager(c2.dummyConnection);
|
||||
await mm2.syncConfigTable();
|
||||
|
||||
const config = {
|
||||
...json,
|
||||
@@ -112,12 +160,11 @@ describe("ModuleManager", async () => {
|
||||
}
|
||||
};
|
||||
await db
|
||||
.updateTable(TABLE_NAME)
|
||||
.set({ json: JSON.stringify(config), version: CURRENT_VERSION })
|
||||
.insertInto(TABLE_NAME)
|
||||
.values({ type: "config", json: JSON.stringify(config), version: CURRENT_VERSION })
|
||||
.execute();
|
||||
|
||||
// run without config given
|
||||
const mm2 = new ModuleManager(c2.dummyConnection);
|
||||
await mm2.build();
|
||||
|
||||
expect(mm2.configs().data.basepath).toBe("/api/data2");
|
||||
@@ -148,50 +195,61 @@ describe("ModuleManager", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
// @todo: check what happens here
|
||||
/*test("blank app, modify deep config", async () => {
|
||||
test("partial config given", async () => {
|
||||
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
|
||||
.get("data")
|
||||
.schema()
|
||||
.patch("entities.test", {
|
||||
fields: {
|
||||
content: {
|
||||
type: "text"
|
||||
}
|
||||
expect(mm.version()).toBe(CURRENT_VERSION);
|
||||
expect(mm.built()).toBe(true);
|
||||
expect(mm.configs().auth.enabled).toBe(true);
|
||||
expect(mm.configs().data.entities.users).toBeDefined();
|
||||
});
|
||||
|
||||
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");
|
||||
|
||||
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({});
|
||||
});*/
|
||||
// @todo: add tests for migrations (check "backup" and new version)
|
||||
});
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 { type ObjectQuery, convert, validate } from "../../../src/core/object/query/object-query";
|
||||
import { deprecated__whereRepoSchema } from "../../../src/data";
|
||||
|
||||
describe("object-query", () => {
|
||||
const q: ObjectQuery = { name: "Michael" };
|
||||
@@ -8,19 +7,6 @@ describe("object-query", () => {
|
||||
const q3: ObjectQuery = { name: "Michael", age: { $gt: 18 } };
|
||||
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 () => {
|
||||
const converted = convert({
|
||||
name: { $eq: "ch" }
|
||||
|
||||
@@ -132,14 +132,47 @@ describe("Mutator simple", async () => {
|
||||
const data = (await em.repository(items).findMany()).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);
|
||||
//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(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 }
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { MediaField } from "../../src";
|
||||
import {
|
||||
BooleanField,
|
||||
DateField,
|
||||
@@ -30,6 +29,7 @@ import {
|
||||
relation,
|
||||
text
|
||||
} from "../../src/data/prototype";
|
||||
import { MediaField } from "../../src/media/MediaField";
|
||||
|
||||
describe("prototype", () => {
|
||||
test("...", () => {
|
||||
@@ -76,7 +76,9 @@ describe("prototype", () => {
|
||||
new DateField("created_at", {
|
||||
type: "datetime"
|
||||
}),
|
||||
// @ts-ignore
|
||||
new MediaField("images", { entity: "posts" }),
|
||||
// @ts-ignore
|
||||
new MediaField("cover", { entity: "posts", max_items: 1 })
|
||||
]);
|
||||
|
||||
|
||||
@@ -39,7 +39,10 @@ describe("AppAuth", () => {
|
||||
test("creates user on register", async () => {
|
||||
const auth = new AppAuth(
|
||||
{
|
||||
enabled: true
|
||||
enabled: true,
|
||||
jwt: {
|
||||
secret: "123456"
|
||||
}
|
||||
},
|
||||
ctx
|
||||
);
|
||||
@@ -57,6 +60,9 @@ describe("AppAuth", () => {
|
||||
disableConsoleLog();
|
||||
const res = await app.request("/password/register", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: "some@body.com",
|
||||
password: "123456"
|
||||
|
||||
+9
-2
@@ -88,11 +88,12 @@ await tsup.build({
|
||||
watch,
|
||||
entry: ["src/index.ts", "src/data/index.ts", "src/core/index.ts", "src/core/utils/index.ts"],
|
||||
outDir: "dist",
|
||||
external: ["bun:test"],
|
||||
external: ["bun:test", "@libsql/client"],
|
||||
metafile: true,
|
||||
platform: "browser",
|
||||
format: ["esm", "cjs"],
|
||||
splitting: false,
|
||||
treeshake: true,
|
||||
loader: {
|
||||
".svg": "dataurl"
|
||||
}
|
||||
@@ -107,11 +108,12 @@ await tsup.build({
|
||||
watch,
|
||||
entry: ["src/ui/index.ts", "src/ui/client/index.ts", "src/ui/main.css"],
|
||||
outDir: "dist/ui",
|
||||
external: ["bun:test"],
|
||||
external: ["bun:test", "react", "react-dom", "use-sync-external-store"],
|
||||
metafile: true,
|
||||
platform: "browser",
|
||||
format: ["esm", "cjs"],
|
||||
splitting: true,
|
||||
treeshake: true,
|
||||
loader: {
|
||||
".svg": "dataurl"
|
||||
},
|
||||
@@ -179,3 +181,8 @@ await tsup.build({
|
||||
platform: "node",
|
||||
format: ["esm", "cjs"]
|
||||
});
|
||||
|
||||
await tsup.build({
|
||||
...baseConfig("astro"),
|
||||
format: ["esm", "cjs"]
|
||||
});
|
||||
|
||||
+43
-36
@@ -3,12 +3,12 @@
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"bin": "./dist/cli/index.js",
|
||||
"version": "0.1.1",
|
||||
"version": "0.3.0",
|
||||
"scripts": {
|
||||
"build:all": "bun run build && bun run build:cli",
|
||||
"dev": "vite",
|
||||
"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",
|
||||
"types": "bun tsc --noEmit",
|
||||
"clean:types": "find ./dist -name '*.d.ts' -delete && rm -f ./dist/tsconfig.tsbuildinfo",
|
||||
@@ -17,20 +17,35 @@
|
||||
"watch:css": "bun tailwindcss --watch -i src/ui/main.css -o ./dist/styles.css",
|
||||
"updater": "bun x npm-check-updates -ui",
|
||||
"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",
|
||||
"dependencies": {
|
||||
"@libsql/client": "^0.14.0",
|
||||
"@tanstack/react-form": "0.19.2",
|
||||
"@sinclair/typebox": "^0.32.34",
|
||||
"kysely": "^0.27.4",
|
||||
"liquidjs": "^10.15.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"hono": "^4.6.12",
|
||||
"fast-xml-parser": "^4.4.0",
|
||||
"@cfworker/json-schema": "^2.0.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"oauth4webapi": "^2.11.1",
|
||||
"aws4fetch": "^1.0.18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aws-sdk/client-s3": "^3.613.0",
|
||||
"@codemirror/lang-html": "^6.4.9",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-liquid": "^6.2.1",
|
||||
"@dagrejs/dagre": "^1.1.4",
|
||||
"@hello-pangea/dnd": "^17.0.0",
|
||||
"@hono/typebox-validator": "^0.2.6",
|
||||
"@hono/vite-dev-server": "^0.17.0",
|
||||
"@hono/zod-validator": "^0.4.1",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@libsql/client": "^0.14.0",
|
||||
"@libsql/kysely-libsql": "^0.4.1",
|
||||
"@mantine/core": "^7.13.4",
|
||||
"@mantine/hooks": "^7.13.4",
|
||||
@@ -38,54 +53,37 @@
|
||||
"@mantine/notifications": "^7.13.5",
|
||||
"@radix-ui/react-scroll-area": "^1.2.0",
|
||||
"@rjsf/core": "^5.22.2",
|
||||
"@sinclair/typebox": "^0.32.34",
|
||||
"@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/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@uiw/react-codemirror": "^4.23.6",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"@xyflow/react": "^12.3.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"esbuild-postcss": "^0.0.4",
|
||||
"node-fetch": "^3.3.2",
|
||||
"jotai": "^2.10.1",
|
||||
"open": "^10.1.0",
|
||||
"openapi-types": "^12.1.3",
|
||||
"postcss": "^8.4.47",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"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-animate": "^1.0.7",
|
||||
"tsup": "^8.3.5",
|
||||
"vite": "^5.4.10",
|
||||
"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": {
|
||||
"react": ">=18",
|
||||
@@ -160,15 +158,24 @@
|
||||
"import": "./dist/adapter/node/index.js",
|
||||
"require": "./dist/adapter/node/index.cjs"
|
||||
},
|
||||
"./adapter/astro": {
|
||||
"types": "./dist/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/manifest.json": "./dist/static/manifest.json"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md",
|
||||
"!dist/*.tsbuildinfo",
|
||||
"!dist/*.map",
|
||||
"!dist/**/*.map",
|
||||
"!dist/metafile*"
|
||||
"!dist/metafile*",
|
||||
"!dist/**/metafile*"
|
||||
]
|
||||
}
|
||||
|
||||
+29
-23
@@ -21,7 +21,7 @@ export class AppBuiltEvent extends Event<{ app: App }> {
|
||||
export const AppEvents = { AppConfigUpdatedEvent, AppBuiltEvent } as const;
|
||||
|
||||
export type CreateAppConfig = {
|
||||
connection:
|
||||
connection?:
|
||||
| Connection
|
||||
| {
|
||||
type: "libsql";
|
||||
@@ -29,7 +29,7 @@ export type CreateAppConfig = {
|
||||
};
|
||||
initialConfig?: InitialModuleConfigs;
|
||||
plugins?: AppPlugin<any>[];
|
||||
options?: ModuleManagerOptions;
|
||||
options?: Omit<ModuleManagerOptions, "initial" | "onUpdated">;
|
||||
};
|
||||
|
||||
export type AppConfig = InitialModuleConfigs;
|
||||
@@ -56,27 +56,6 @@ export class App<DB = any> {
|
||||
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() {
|
||||
return this.modules.ctx().emgr;
|
||||
}
|
||||
@@ -147,4 +126,31 @@ export class App<DB = any> {
|
||||
toJSON(secrets?: boolean) {
|
||||
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,30 @@
|
||||
import { Api, type ApiOptions, App, type CreateAppConfig } from "bknd";
|
||||
|
||||
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: CreateAppConfig) {
|
||||
return async (args: TAstro) => {
|
||||
if (!app) {
|
||||
app = App.create(config);
|
||||
|
||||
await app.build();
|
||||
}
|
||||
return app.fetch(args.request);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./astro.adapter";
|
||||
@@ -1,55 +1,59 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import path from "node:path";
|
||||
import { App, type CreateAppConfig } from "bknd";
|
||||
import { LibsqlConnection } from "bknd/data";
|
||||
import type { Serve, ServeOptions } from "bun";
|
||||
import { serveStatic } from "hono/bun";
|
||||
|
||||
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 function serve(_config: Partial<CreateAppConfig> = {}, distPath?: string) {
|
||||
let app: App;
|
||||
export async function createApp(_config: Partial<CreateAppConfig> = {}, distPath?: string) {
|
||||
const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static");
|
||||
let app: App;
|
||||
|
||||
return async (req: Request) => {
|
||||
if (!app) {
|
||||
const connection = await getConnection(_config.connection);
|
||||
app = App.create({
|
||||
..._config,
|
||||
connection
|
||||
});
|
||||
if (!app) {
|
||||
app = App.create(_config);
|
||||
|
||||
app.emgr.on(
|
||||
"app-built",
|
||||
async () => {
|
||||
app.modules.server.get(
|
||||
"/*",
|
||||
serveStatic({
|
||||
root
|
||||
})
|
||||
);
|
||||
app.registerAdminController();
|
||||
},
|
||||
"sync"
|
||||
);
|
||||
app.emgr.on(
|
||||
"app-built",
|
||||
async () => {
|
||||
app.modules.server.get(
|
||||
"/*",
|
||||
serveStatic({
|
||||
root
|
||||
})
|
||||
);
|
||||
app.registerAdminController();
|
||||
},
|
||||
"sync"
|
||||
);
|
||||
|
||||
await app.build();
|
||||
}
|
||||
await app.build();
|
||||
}
|
||||
|
||||
return app.fetch(req);
|
||||
};
|
||||
return app;
|
||||
}
|
||||
|
||||
export type BunAdapterOptions = Omit<ServeOptions, "fetch"> &
|
||||
CreateAppConfig & {
|
||||
distPath?: string;
|
||||
};
|
||||
|
||||
export function serve({
|
||||
distPath,
|
||||
connection,
|
||||
initialConfig,
|
||||
plugins,
|
||||
options,
|
||||
port = 1337,
|
||||
...serveOptions
|
||||
}: BunAdapterOptions = {}) {
|
||||
Bun.serve({
|
||||
...serveOptions,
|
||||
port,
|
||||
fetch: async (request: Request) => {
|
||||
const app = await createApp({ connection, initialConfig, plugins, options }, distPath);
|
||||
return app.fetch(request);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Server is running on http://localhost:${port}`);
|
||||
}
|
||||
|
||||
@@ -189,7 +189,11 @@ export class DurableBkndApp extends DurableObject {
|
||||
const config = options.config;
|
||||
|
||||
// change protocol to websocket if libsql
|
||||
if ("type" in config.connection && config.connection.type === "libsql") {
|
||||
if (
|
||||
config?.connection &&
|
||||
"type" in config.connection &&
|
||||
config.connection.type === "libsql"
|
||||
) {
|
||||
config.connection.config.protocol = "wss";
|
||||
}
|
||||
|
||||
|
||||
@@ -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 "./AdminPage";
|
||||
|
||||
@@ -2,51 +2,34 @@ import path from "node:path";
|
||||
import { serve as honoServe } from "@hono/node-server";
|
||||
import { serveStatic } from "@hono/node-server/serve-static";
|
||||
import { App, type CreateAppConfig } from "bknd";
|
||||
import { LibsqlConnection } from "bknd/data";
|
||||
|
||||
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 = {
|
||||
export type NodeAdapterOptions = CreateAppConfig & {
|
||||
relativeDistPath?: string;
|
||||
port?: number;
|
||||
hostname?: string;
|
||||
listener?: Parameters<typeof honoServe>[1];
|
||||
};
|
||||
|
||||
export function serve(_config: Partial<CreateAppConfig> = {}, options: NodeAdapterOptions = {}) {
|
||||
export function serve({
|
||||
relativeDistPath,
|
||||
port = 1337,
|
||||
hostname,
|
||||
listener,
|
||||
...config
|
||||
}: NodeAdapterOptions = {}) {
|
||||
const root = path.relative(
|
||||
process.cwd(),
|
||||
path.resolve(options.relativeDistPath ?? "./node_modules/bknd/dist", "static")
|
||||
path.resolve(relativeDistPath ?? "./node_modules/bknd/dist", "static")
|
||||
);
|
||||
let app: App;
|
||||
|
||||
honoServe(
|
||||
{
|
||||
port: options.port ?? 1337,
|
||||
hostname: options.hostname,
|
||||
port,
|
||||
hostname,
|
||||
fetch: async (req: Request) => {
|
||||
if (!app) {
|
||||
const connection = await getConnection(_config.connection);
|
||||
app = App.create({
|
||||
..._config,
|
||||
connection
|
||||
});
|
||||
app = App.create(config);
|
||||
|
||||
app.emgr.on(
|
||||
"app-built",
|
||||
@@ -68,6 +51,9 @@ export function serve(_config: Partial<CreateAppConfig> = {}, options: NodeAdapt
|
||||
return app.fetch(req);
|
||||
}
|
||||
},
|
||||
options.listener
|
||||
(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";
|
||||
|
||||
export function adminPage() {
|
||||
export function adminPage(props?: BkndAdminProps) {
|
||||
const Admin = lazy(() => import("bknd/ui").then((mod) => ({ default: mod.Admin })));
|
||||
return () => {
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
@@ -12,7 +13,7 @@ export function adminPage() {
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
<Admin />
|
||||
<Admin {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,10 +11,21 @@ export class AuthController implements ClassController {
|
||||
|
||||
getMiddleware: MiddlewareHandler = async (c, next) => {
|
||||
// @todo: ONLY HOTFIX
|
||||
// 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 (ext) {
|
||||
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);
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "core/utils";
|
||||
import type { Context, Hono } from "hono";
|
||||
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 { 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;
|
||||
}
|
||||
|
||||
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> {
|
||||
@@ -249,6 +254,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
||||
}
|
||||
}
|
||||
|
||||
// @todo: move this to a server helper
|
||||
isJsonRequest(c: Context): boolean {
|
||||
//return c.req.header("Content-Type") === "application/x-www-form-urlencoded";
|
||||
return c.req.header("Content-Type") === "application/json";
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { ServeStaticOptions } from "@hono/node-server/serve-static";
|
||||
import { type Config, createClient } from "@libsql/client/node";
|
||||
import { Connection, LibsqlConnection, SqliteLocalConnection } from "data";
|
||||
import type { Config } from "@libsql/client/node";
|
||||
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 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 }) {
|
||||
const port = options.port;
|
||||
console.log("running on", server, port);
|
||||
console.log(`(using ${server} serve)`);
|
||||
|
||||
switch (server) {
|
||||
case "node": {
|
||||
// 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);
|
||||
}
|
||||
|
||||
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:"));
|
||||
const url = `http://localhost:${port}`;
|
||||
console.log(`Server listening on ${url}`);
|
||||
await open(url);
|
||||
}
|
||||
|
||||
export async function getConfigPath(filePath?: string) {
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import type { Config } from "@libsql/client/node";
|
||||
import { App } from "App";
|
||||
import { App, type CreateAppConfig } from "App";
|
||||
import type { BkndConfig } from "adapter";
|
||||
import type { CliCommand } from "cli/types";
|
||||
import { Option } from "commander";
|
||||
import type { Connection } from "data";
|
||||
import {
|
||||
PLATFORMS,
|
||||
type Platform,
|
||||
attachServeStatic,
|
||||
getConfigPath,
|
||||
getConnection,
|
||||
getHtml,
|
||||
startServer
|
||||
} from "./platform";
|
||||
|
||||
@@ -41,14 +38,14 @@ export const run: CliCommand = (program) => {
|
||||
};
|
||||
|
||||
type MakeAppConfig = {
|
||||
connection: Connection;
|
||||
connection?: CreateAppConfig["connection"];
|
||||
server?: { platform?: Platform };
|
||||
setAdminHtml?: boolean;
|
||||
onBuilt?: (app: App) => Promise<void>;
|
||||
};
|
||||
|
||||
async function makeApp(config: MakeAppConfig) {
|
||||
const app = new App(config.connection);
|
||||
const app = App.create({ connection: config.connection });
|
||||
|
||||
app.emgr.on(
|
||||
"app-built",
|
||||
@@ -99,9 +96,9 @@ async function action(options: {
|
||||
|
||||
let app: App;
|
||||
if (options.dbUrl || !configFilePath) {
|
||||
const connection = getConnection(
|
||||
options.dbUrl ? { url: options.dbUrl, authToken: options.dbToken } : undefined
|
||||
);
|
||||
const connection = options.dbUrl
|
||||
? { type: "libsql" as const, config: { url: options.dbUrl, authToken: options.dbToken } }
|
||||
: undefined;
|
||||
app = await makeApp({ connection, server: { platform: options.server } });
|
||||
} else {
|
||||
console.log("Using config from:", configFilePath);
|
||||
|
||||
@@ -27,6 +27,10 @@ export class BkndError extends Error {
|
||||
super(message);
|
||||
}
|
||||
|
||||
static with(message: string, details?: Record<string, any>, type?: string) {
|
||||
throw new BkndError(message, details, type);
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
type: this.type ?? "unknown",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export { Endpoint, type RequestResponse, type Middleware } from "./server/Endpoint";
|
||||
export { zValidator } from "./server/lib/zValidator";
|
||||
import type { Hono, MiddlewareHandler } from "hono";
|
||||
|
||||
export { tbValidator } from "./server/lib/tbValidator";
|
||||
export { Exception, BkndError } from "./errors";
|
||||
export { isDebug } from "./env";
|
||||
@@ -11,7 +11,6 @@ export {
|
||||
type TemplateTypes,
|
||||
type SimpleRendererOptions
|
||||
} from "./template/SimpleRenderer";
|
||||
export { Controller, type ClassController } from "./server/Controller";
|
||||
export { SchemaObject } from "./object/SchemaObject";
|
||||
export { DebugLogger } from "./utils/DebugLogger";
|
||||
export { Permission } from "./security/Permission";
|
||||
@@ -26,3 +25,10 @@ export {
|
||||
isBooleanLike
|
||||
} from "./object/query/query";
|
||||
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>;
|
||||
}
|
||||
|
||||
@@ -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,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>;
|
||||
});
|
||||
@@ -5,10 +5,21 @@ const _oldConsoles = {
|
||||
error: console.error
|
||||
};
|
||||
|
||||
export async function withDisabledConsole<R>(
|
||||
fn: () => Promise<R>,
|
||||
severities: ConsoleSeverity[] = ["log"]
|
||||
): Promise<R> {
|
||||
const enable = disableConsoleLog(severities);
|
||||
const result = await fn();
|
||||
enable();
|
||||
return result;
|
||||
}
|
||||
|
||||
export function disableConsoleLog(severities: ConsoleSeverity[] = ["log"]) {
|
||||
severities.forEach((severity) => {
|
||||
console[severity] = () => null;
|
||||
});
|
||||
return 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);
|
||||
mark(newObj, false);
|
||||
return newObj;
|
||||
return newObj as O;
|
||||
}
|
||||
|
||||
export function mark(obj: any, validated = true) {
|
||||
|
||||
@@ -369,9 +369,9 @@ export class DataController implements ClassController {
|
||||
return c.notFound();
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -41,16 +41,18 @@ export type DbFunctions = {
|
||||
>;
|
||||
};
|
||||
|
||||
export abstract class Connection {
|
||||
cls = "bknd:connection";
|
||||
kysely: Kysely<any>;
|
||||
const CONN_SYMBOL = Symbol.for("bknd:connection");
|
||||
|
||||
export abstract class Connection<DB = any> {
|
||||
kysely: Kysely<DB>;
|
||||
|
||||
constructor(
|
||||
kysely: Kysely<any>,
|
||||
kysely: Kysely<DB>,
|
||||
public fn: Partial<DbFunctions> = {},
|
||||
protected plugins: KyselyPlugin[] = []
|
||||
) {
|
||||
this.kysely = kysely;
|
||||
this[CONN_SYMBOL] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,8 +60,9 @@ export abstract class Connection {
|
||||
* coming from different places
|
||||
* @param conn
|
||||
*/
|
||||
static isConnection(conn: any): conn is Connection {
|
||||
return conn?.cls === "bknd:connection";
|
||||
static isConnection(conn: unknown): conn is Connection {
|
||||
if (!conn) return false;
|
||||
return conn[CONN_SYMBOL] === true;
|
||||
}
|
||||
|
||||
getIntrospector(): ConnectionIntrospector {
|
||||
|
||||
@@ -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 { type DatabaseIntrospector, Kysely, ParseJSONResultsPlugin, sql } from "kysely";
|
||||
import { FilterNumericKeysPlugin } from "../plugins/FilterNumericKeysPlugin";
|
||||
@@ -8,9 +8,7 @@ import { SqliteConnection } from "./SqliteConnection";
|
||||
import { SqliteIntrospector } from "./SqliteIntrospector";
|
||||
|
||||
export const LIBSQL_PROTOCOLS = ["wss", "https", "libsql"] as const;
|
||||
export type LibSqlCredentials = {
|
||||
url: string;
|
||||
authToken?: string;
|
||||
export type LibSqlCredentials = Config & {
|
||||
protocol?: (typeof LIBSQL_PROTOCOLS)[number];
|
||||
};
|
||||
|
||||
|
||||
@@ -250,7 +250,7 @@ export class Mutator<DB> implements EmitsEvents {
|
||||
}
|
||||
|
||||
// @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<EntityData>> {
|
||||
const entity = this.entity;
|
||||
|
||||
const qb = this.appendWhere(this.conn.deleteFrom(entity.name), where).returning(
|
||||
@@ -267,4 +267,30 @@ export class Mutator<DB> implements EmitsEvents {
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
async updateWhere(
|
||||
data: EntityData,
|
||||
where?: RepoQuery["where"]
|
||||
): Promise<MutatorResponse<EntityData>> {
|
||||
const entity = this.entity;
|
||||
|
||||
const validatedData = await this.getValidatedData(data, "update");
|
||||
|
||||
/*await this.emgr.emit(
|
||||
new Mutator.Events.MutatorUpdateBefore({ entity, entityId: id, data: validatedData })
|
||||
);*/
|
||||
|
||||
const query = this.appendWhere(this.conn.updateTable(entity.name), where)
|
||||
.set(validatedData)
|
||||
//.where(entity.id().name, "=", id)
|
||||
.returning(entity.getSelect());
|
||||
|
||||
const res = await this.many(query);
|
||||
|
||||
/*await this.emgr.emit(
|
||||
new Mutator.Events.MutatorUpdateAfter({ entity, entityId: id, data: res.data })
|
||||
);*/
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,8 +162,7 @@ export class Repository<DB = any, TB extends keyof DB = any> implements EmitsEve
|
||||
protected async performQuery(qb: RepositoryQB): Promise<RepositoryResponse> {
|
||||
const entity = this.entity;
|
||||
const compiled = qb.compile();
|
||||
/*const { sql, parameters } = qb.compile();
|
||||
console.log("many", sql, parameters);*/
|
||||
//console.log("performQuery", compiled.sql, compiled.parameters);
|
||||
|
||||
const start = performance.now();
|
||||
const selector = (as = "count") => this.conn.fn.countAll<number>().as(as);
|
||||
@@ -202,7 +201,10 @@ export class Repository<DB = any, TB extends keyof DB = any> implements EmitsEve
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("many error", e, compiled);
|
||||
if (e instanceof Error) {
|
||||
console.error("[ERROR] Repository.performQuery", e.message);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -263,6 +265,7 @@ export class Repository<DB = any, TB extends keyof DB = any> implements EmitsEve
|
||||
qb = qb.orderBy(aliased(options.sort.by), options.sort.dir);
|
||||
}
|
||||
|
||||
//console.log("options", { _options, options, exclude_options });
|
||||
return { qb, options };
|
||||
}
|
||||
|
||||
@@ -286,14 +289,11 @@ export class Repository<DB = any, TB extends keyof DB = any> implements EmitsEve
|
||||
where: RepoQuery["where"],
|
||||
_options?: Partial<Omit<RepoQuery, "where" | "limit" | "offset">>
|
||||
): Promise<RepositoryResponse<DB[TB] | undefined>> {
|
||||
const { qb, options } = this.buildQuery(
|
||||
{
|
||||
..._options,
|
||||
where,
|
||||
limit: 1
|
||||
},
|
||||
["offset", "sort"]
|
||||
);
|
||||
const { qb, options } = this.buildQuery({
|
||||
..._options,
|
||||
where,
|
||||
limit: 1
|
||||
});
|
||||
|
||||
return this.single(qb, options) as any;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import type {
|
||||
SelectQueryBuilder,
|
||||
UpdateQueryBuilder
|
||||
} from "kysely";
|
||||
import type { RepositoryQB } from "./Repository";
|
||||
|
||||
type Builder = ExpressionBuilder<any, any>;
|
||||
type Wrapper = ExpressionWrapper<any, any, any>;
|
||||
|
||||
@@ -236,8 +236,8 @@ export abstract class Field<
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
//name: this.name,
|
||||
type: this.type,
|
||||
// @todo: current workaround because of fixed string type
|
||||
type: this.type as any,
|
||||
config: this.config
|
||||
};
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export class JsonSchemaField<
|
||||
|
||||
if (parentValid) {
|
||||
// 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);
|
||||
return true;
|
||||
}
|
||||
@@ -65,6 +65,7 @@ export class JsonSchemaField<
|
||||
} else {
|
||||
//console.log("jsonschema:invalid", this.name, value, context);
|
||||
}
|
||||
//console.log("jsonschema:invalid:fromParent", this.name, value, context);
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -110,9 +111,13 @@ export class JsonSchemaField<
|
||||
): Promise<string | undefined> {
|
||||
const value = await super.transformPersist(_value, em, context);
|
||||
if (this.nullish(value)) return value;
|
||||
//console.log("jsonschema:transformPersist", this.name, _value, context);
|
||||
|
||||
if (!this.isValid(value)) {
|
||||
//console.error("jsonschema:transformPersist:invalid", 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();
|
||||
|
||||
@@ -4,6 +4,7 @@ export * from "./fields";
|
||||
export * from "./entities";
|
||||
export * from "./relations";
|
||||
export * from "./schema/SchemaManager";
|
||||
export * from "./prototype";
|
||||
|
||||
export {
|
||||
type RepoQuery,
|
||||
@@ -12,8 +13,6 @@ export {
|
||||
whereSchema
|
||||
} from "./server/data-query-impl";
|
||||
|
||||
export { whereRepoSchema as deprecated__whereRepoSchema } from "./server/query";
|
||||
|
||||
export { Connection } from "./connection/Connection";
|
||||
export { LibsqlConnection, type LibSqlCredentials } from "./connection/LibsqlConnection";
|
||||
export { SqliteConnection } from "./connection/SqliteConnection";
|
||||
|
||||
@@ -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>;
|
||||
+2
-3
@@ -1,6 +1,5 @@
|
||||
export { App, type AppConfig, type CreateAppConfig } from "./App";
|
||||
export { App, createApp, AppEvents, type AppConfig, type CreateAppConfig } from "./App";
|
||||
|
||||
export { MediaField } from "media/MediaField";
|
||||
export {
|
||||
getDefaultConfig,
|
||||
getDefaultSchema,
|
||||
@@ -8,5 +7,5 @@ export {
|
||||
type ModuleSchemas
|
||||
} from "modules/ModuleManager";
|
||||
|
||||
export * from "./adapter";
|
||||
export type * from "./adapter";
|
||||
export { Api, type ApiOptions } from "./Api";
|
||||
|
||||
@@ -181,7 +181,7 @@ export class MediaController implements ClassController {
|
||||
if (ids_to_delete.length > 0) {
|
||||
await this.media.em
|
||||
.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 });
|
||||
|
||||
+191
-109
@@ -1,12 +1,33 @@
|
||||
import { Diff } from "@sinclair/typebox/value";
|
||||
import { Guard } from "auth";
|
||||
import { DebugLogger, isDebug } from "core";
|
||||
import { BkndError, DebugLogger, Exception, isDebug } from "core";
|
||||
import { EventManager } from "core/events";
|
||||
import { Default, type Static, objectEach, transformObject } from "core/utils";
|
||||
import { type Connection, EntityManager } from "data";
|
||||
import { clone, diff } from "core/object/diff";
|
||||
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 { type Kysely, sql } from "kysely";
|
||||
import { CURRENT_VERSION, TABLE_NAME, migrate, migrateSchema } from "modules/migrations";
|
||||
import type { Kysely } from "kysely";
|
||||
import { mergeWith } from "lodash-es";
|
||||
import { CURRENT_VERSION, TABLE_NAME, migrate } from "modules/migrations";
|
||||
import { AppServer } from "modules/server/AppServer";
|
||||
import { AppAuth } from "../auth/AppAuth";
|
||||
import { AppData } from "../data/AppData";
|
||||
@@ -37,10 +58,13 @@ export type ModuleSchemas = {
|
||||
export type ModuleConfigs = {
|
||||
[K in keyof ModuleSchemas]: Static<ModuleSchemas[K]>;
|
||||
};
|
||||
type PartialRec<T> = { [P in keyof T]?: PartialRec<T[P]> };
|
||||
|
||||
export type InitialModuleConfigs = {
|
||||
version: number;
|
||||
} & Partial<ModuleConfigs>;
|
||||
export type InitialModuleConfigs =
|
||||
| ({
|
||||
version: number;
|
||||
} & ModuleConfigs)
|
||||
| PartialRec<ModuleConfigs>;
|
||||
|
||||
export type ModuleManagerOptions = {
|
||||
initial?: InitialModuleConfigs;
|
||||
@@ -51,6 +75,7 @@ export type ModuleManagerOptions = {
|
||||
) => Promise<void>;
|
||||
// base path for the hono instance
|
||||
basePath?: string;
|
||||
trustFetched?: boolean;
|
||||
};
|
||||
|
||||
type ConfigTable<Json = ModuleConfigs> = {
|
||||
@@ -61,8 +86,36 @@ type ConfigTable<Json = ModuleConfigs> = {
|
||||
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>;
|
||||
type T_INTERNAL_EM = {
|
||||
__bknd: ConfigTable2;
|
||||
};
|
||||
|
||||
// @todo: cleanup old diffs on upgrade
|
||||
// @todo: cleanup multiple backups on upgrade
|
||||
export class ModuleManager {
|
||||
private modules: Modules;
|
||||
// internal em for __bknd config table
|
||||
__em!: EntityManager<T_INTERNAL_EM>;
|
||||
// ctx for modules
|
||||
em!: EntityManager<any>;
|
||||
server!: Hono;
|
||||
emgr!: EventManager;
|
||||
@@ -71,28 +124,32 @@ export class ModuleManager {
|
||||
private _version: number = 0;
|
||||
private _built = 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(
|
||||
private readonly connection: Connection,
|
||||
private options?: Partial<ModuleManagerOptions>
|
||||
) {
|
||||
this.__em = new EntityManager([__bknd], this.connection);
|
||||
this.modules = {} as Modules;
|
||||
this.emgr = new EventManager();
|
||||
const context = this.ctx(true);
|
||||
let initial = {} as Partial<ModuleConfigs>;
|
||||
|
||||
if (options?.initial) {
|
||||
const { version, ...initialConfig } = options.initial;
|
||||
if (version && initialConfig) {
|
||||
if ("version" in options.initial) {
|
||||
const { version, ...initialConfig } = options.initial;
|
||||
this._version = version;
|
||||
initial = initialConfig;
|
||||
initial = stripMark(initialConfig);
|
||||
|
||||
this._provided = true;
|
||||
this._booted_with = "provided";
|
||||
} else {
|
||||
throw new Error("Initial was provided, but it needs a version!");
|
||||
initial = mergeWith(getDefaultConfig(), options.initial);
|
||||
this._booted_with = "partial";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +177,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() {
|
||||
this.server = new Hono();
|
||||
if (this.options?.basePath) {
|
||||
@@ -153,27 +229,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> {
|
||||
this.logger.context("fetch").log("fetching");
|
||||
|
||||
const startTime = performance.now();
|
||||
const result = await this.db
|
||||
.selectFrom(this.table)
|
||||
.selectAll()
|
||||
.where("type", "=", "config")
|
||||
.orderBy("version", "desc")
|
||||
.executeTakeFirstOrThrow();
|
||||
// disabling console log, because the table might not exist yet
|
||||
return await withDisabledConsole(async () => {
|
||||
const startTime = performance.now();
|
||||
const { data: result } = await this.repo().findOne(
|
||||
{ type: "config" },
|
||||
{
|
||||
sort: { by: "version", dir: "desc" }
|
||||
}
|
||||
);
|
||||
|
||||
this.logger.log("took", performance.now() - startTime, "ms", result).clear();
|
||||
return result;
|
||||
if (!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() {
|
||||
@@ -181,65 +256,75 @@ export class ModuleManager {
|
||||
const configs = this.configs();
|
||||
const version = this.version();
|
||||
|
||||
const json = JSON.stringify(configs) as any;
|
||||
const state = await this.fetch();
|
||||
try {
|
||||
const state = await this.fetch();
|
||||
this.logger.log("fetched version", state.version);
|
||||
|
||||
if (state.version !== version) {
|
||||
// @todo: mark all others as "backup"
|
||||
this.logger.log("version conflict, storing new version", state.version, version);
|
||||
await this.db
|
||||
.insertInto(this.table)
|
||||
.values({
|
||||
version,
|
||||
if (state.version !== version) {
|
||||
// @todo: mark all others as "backup"
|
||||
this.logger.log("version conflict, storing new version", state.version, version);
|
||||
await this.mutator().insertOne({
|
||||
version: state.version,
|
||||
type: "backup",
|
||||
json: configs
|
||||
});
|
||||
await this.mutator().insertOne({
|
||||
version: version,
|
||||
type: "config",
|
||||
json
|
||||
})
|
||||
.execute();
|
||||
} else {
|
||||
this.logger.log("version matches");
|
||||
json: configs
|
||||
});
|
||||
} else {
|
||||
this.logger.log("version matches");
|
||||
|
||||
const diff = Diff(state.json, JSON.parse(json));
|
||||
this.logger.log("checking diff", diff);
|
||||
// clean configs because of Diff() function
|
||||
const diffs = diff(state.json, clone(configs));
|
||||
this.logger.log("checking diff", diffs);
|
||||
|
||||
if (diff.length > 0) {
|
||||
// store diff
|
||||
await this.db
|
||||
.insertInto(this.table)
|
||||
.values({
|
||||
if (diff.length > 0) {
|
||||
// store diff
|
||||
await this.mutator().insertOne({
|
||||
version,
|
||||
type: "diff",
|
||||
json: JSON.stringify(diff) as any
|
||||
})
|
||||
.execute();
|
||||
json: clone(diffs)
|
||||
});
|
||||
|
||||
await this.db
|
||||
.updateTable(this.table)
|
||||
.set({ version, json, updated_at: sql`CURRENT_TIMESTAMP` })
|
||||
.where((eb) => eb.and([eb("type", "=", "config"), eb("version", "=", version)]))
|
||||
.execute();
|
||||
// store new version
|
||||
await this.mutator().updateWhere(
|
||||
{
|
||||
version,
|
||||
json: configs,
|
||||
updated_at: new Date()
|
||||
},
|
||||
{
|
||||
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 {
|
||||
this.logger.log("no diff, not saving");
|
||||
console.error("Aborting");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// cleanup
|
||||
/*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);*/
|
||||
// @todo: cleanup old versions?
|
||||
|
||||
this.logger.clear();
|
||||
return this;
|
||||
@@ -250,6 +335,8 @@ export class ModuleManager {
|
||||
|
||||
if (this.version() < CURRENT_VERSION) {
|
||||
this.logger.log("there are migrations, verify version");
|
||||
// sync __bknd table
|
||||
await this.syncConfigTable();
|
||||
|
||||
// modules must be built before migration
|
||||
await this.buildModules({ graceful: true });
|
||||
@@ -264,14 +351,7 @@ export class ModuleManager {
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.logger.clear(); // fetch couldn't clear
|
||||
|
||||
// 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}`);
|
||||
}
|
||||
throw new Error(`Version is ${this.version()}, fetch failed: ${e.message}`);
|
||||
}
|
||||
|
||||
this.logger.log("now migrating");
|
||||
@@ -289,9 +369,6 @@ export class ModuleManager {
|
||||
configs = _configs;
|
||||
|
||||
this.setConfigs(configs);
|
||||
/* objectEach(configs, (config, key) => {
|
||||
this.get(key as any).setConfig(config);
|
||||
}); */
|
||||
|
||||
this._version = version;
|
||||
this.logger.log("migrated to", version);
|
||||
@@ -325,10 +402,11 @@ export class ModuleManager {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log("building");
|
||||
const ctx = this.ctx(true);
|
||||
for (const key in this.modules) {
|
||||
this.logger.log(`building "${key}"`);
|
||||
await this.modules[key].setContext(ctx).build();
|
||||
this.logger.log("built", key);
|
||||
}
|
||||
|
||||
this._built = true;
|
||||
@@ -337,37 +415,41 @@ export class ModuleManager {
|
||||
|
||||
async build() {
|
||||
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 (this.version() === 0) {
|
||||
this.logger.context("build no config").log("version is 0");
|
||||
this.logger.context("no version").log("version is 0");
|
||||
try {
|
||||
const result = await this.fetch();
|
||||
|
||||
// set version and config from fetched
|
||||
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);
|
||||
} catch (e: any) {
|
||||
this.logger.clear(); // fetch couldn't clear
|
||||
|
||||
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
|
||||
// it's up to date because we use default configs (no fetch result)
|
||||
await this.buildModules();
|
||||
await this.save();
|
||||
// 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)
|
||||
this._version = CURRENT_VERSION;
|
||||
await this.syncConfigTable();
|
||||
await this.buildModules();
|
||||
await this.save();
|
||||
|
||||
this.logger.clear();
|
||||
return this;
|
||||
} else {
|
||||
throw e;
|
||||
//throw new Error("Issues connecting to the database. Reason: " + e.message);
|
||||
}
|
||||
this.logger.clear();
|
||||
return this;
|
||||
}
|
||||
this.logger.clear();
|
||||
}
|
||||
|
||||
@@ -16,26 +16,8 @@ export type Migration = {
|
||||
export const migrations: Migration[] = [
|
||||
{
|
||||
version: 1,
|
||||
schema: true,
|
||||
up: async (config, { db }) => {
|
||||
//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;
|
||||
}
|
||||
//schema: true,
|
||||
up: async (config) => config
|
||||
},
|
||||
{
|
||||
version: 2,
|
||||
@@ -45,12 +27,8 @@ export const migrations: Migration[] = [
|
||||
},
|
||||
{
|
||||
version: 3,
|
||||
schema: true,
|
||||
up: async (config, { db }) => {
|
||||
await db.schema.alterTable(TABLE_NAME).addColumn("deleted_at", "datetime").execute();
|
||||
|
||||
return config;
|
||||
}
|
||||
//schema: true,
|
||||
up: async (config) => config
|
||||
},
|
||||
{
|
||||
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;
|
||||
@@ -127,28 +112,6 @@ export async function migrateTo(
|
||||
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(
|
||||
current: number,
|
||||
config: GenericConfigObject,
|
||||
|
||||
@@ -10,9 +10,11 @@ import * as SystemPermissions from "modules/permissions";
|
||||
|
||||
const htmlBkndContextReplace = "<!-- BKND_CONTEXT -->";
|
||||
|
||||
// @todo: add migration to remove admin path from config
|
||||
export type AdminControllerOptions = {
|
||||
basepath?: string;
|
||||
html?: string;
|
||||
forceDev?: boolean;
|
||||
forceDev?: boolean | { mainPath: string };
|
||||
};
|
||||
|
||||
export class AdminController implements ClassController {
|
||||
@@ -25,8 +27,12 @@ export class AdminController implements ClassController {
|
||||
return this.app.modules.ctx();
|
||||
}
|
||||
|
||||
get basepath() {
|
||||
return this.options.basepath ?? "/";
|
||||
}
|
||||
|
||||
private withBasePath(route: string = "") {
|
||||
return (this.app.modules.configs().server.admin.basepath + route).replace(/\/+$/, "/");
|
||||
return (this.basepath + route).replace(/\/+$/, "/");
|
||||
}
|
||||
|
||||
getController(): Hono<any> {
|
||||
@@ -102,7 +108,10 @@ export class AdminController implements ClassController {
|
||||
|
||||
if (this.options.html) {
|
||||
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(
|
||||
@@ -113,6 +122,10 @@ export class AdminController implements ClassController {
|
||||
|
||||
const configs = this.app.modules.configs();
|
||||
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 = {
|
||||
js: "main.js",
|
||||
@@ -166,13 +179,14 @@ export class AdminController implements ClassController {
|
||||
)}
|
||||
</head>
|
||||
<body>
|
||||
<div id="root" />
|
||||
<div id="app" />
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: bknd_context
|
||||
}}
|
||||
/>
|
||||
{!isProd && <script type="module" src="/src/ui/main.tsx" />}
|
||||
{!isProd && <script type="module" src={mainPath} />}
|
||||
</body>
|
||||
</html>
|
||||
</Fragment>
|
||||
|
||||
+17
-6
@@ -1,26 +1,37 @@
|
||||
import { MantineProvider } from "@mantine/core";
|
||||
import { Notifications } from "@mantine/notifications";
|
||||
import type { ModuleConfigs } from "modules";
|
||||
import React from "react";
|
||||
import { BkndProvider, useBknd } from "ui/client/bknd";
|
||||
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 { BkndModalsProvider } from "./modals";
|
||||
import { Routes } from "./routes";
|
||||
|
||||
export type BkndAdminProps = {
|
||||
baseUrl?: string;
|
||||
withProvider?: boolean;
|
||||
// @todo: add admin config override
|
||||
withProvider?: boolean | ClientProviderProps;
|
||||
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 = (
|
||||
<BkndProvider>
|
||||
<BkndProvider adminOverride={config}>
|
||||
<AdminInternal />
|
||||
</BkndProvider>
|
||||
);
|
||||
return withProvider ? (
|
||||
<ClientProvider baseUrl={baseUrlOverride}>{Component}</ClientProvider>
|
||||
<ClientProvider
|
||||
baseUrl={baseUrlOverride}
|
||||
{...(typeof withProvider === "object" ? withProvider : {})}
|
||||
>
|
||||
{Component}
|
||||
</ClientProvider>
|
||||
) : (
|
||||
Component
|
||||
);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
//import { notifications } from "@mantine/notifications";
|
||||
import { getDefaultConfig, getDefaultSchema } from "modules/ModuleManager";
|
||||
import { createContext, startTransition, useContext, useEffect, useRef, useState } from "react";
|
||||
import type { ModuleConfigs, ModuleSchemas } from "../../modules";
|
||||
@@ -14,6 +13,7 @@ type BkndContext = {
|
||||
requireSecrets: () => Promise<void>;
|
||||
actions: ReturnType<typeof getSchemaActions>;
|
||||
app: AppReduced;
|
||||
adminOverride?: ModuleConfigs["server"]["admin"];
|
||||
};
|
||||
|
||||
const BkndContext = createContext<BkndContext>(undefined!);
|
||||
@@ -21,8 +21,9 @@ export type { TSchemaActions };
|
||||
|
||||
export function BkndProvider({
|
||||
includeSecrets = false,
|
||||
adminOverride,
|
||||
children
|
||||
}: { includeSecrets?: boolean; children: any }) {
|
||||
}: { includeSecrets?: boolean; children: any } & Pick<BkndContext, "adminOverride">) {
|
||||
const [withSecrets, setWithSecrets] = useState<boolean>(includeSecrets);
|
||||
const [schema, setSchema] =
|
||||
useState<Pick<BkndContext, "version" | "schema" | "config" | "permissions">>();
|
||||
@@ -64,6 +65,13 @@ export function BkndProvider({
|
||||
permissions: []
|
||||
} as any);
|
||||
|
||||
if (adminOverride) {
|
||||
schema.config.server.admin = {
|
||||
...schema.config.server.admin,
|
||||
...adminOverride
|
||||
};
|
||||
}
|
||||
|
||||
startTransition(() => {
|
||||
setSchema(schema);
|
||||
setWithSecrets(_includeSecrets);
|
||||
@@ -86,26 +94,12 @@ export function BkndProvider({
|
||||
const actions = getSchemaActions({ client, setSchema, reloadSchema });
|
||||
|
||||
return (
|
||||
<BkndContext.Provider value={{ ...schema, actions, requireSecrets, app }}>
|
||||
<BkndContext.Provider value={{ ...schema, actions, requireSecrets, app, adminOverride }}>
|
||||
{children}
|
||||
</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 {
|
||||
const ctx = useContext(BkndContext);
|
||||
if (withSecrets) ctx.requireSecrets();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import type { TApiUser } from "Api";
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
import { useBkndWindowContext } from "ui/client/BkndProvider";
|
||||
//import { useBkndWindowContext } from "ui/client/BkndProvider";
|
||||
import { AppQueryClient } from "./utils/AppQueryClient";
|
||||
|
||||
const ClientContext = createContext<{ baseUrl: string; client: AppQueryClient }>({
|
||||
@@ -17,11 +17,13 @@ export const queryClient = new QueryClient({
|
||||
}
|
||||
});
|
||||
|
||||
export const ClientProvider = ({
|
||||
children,
|
||||
baseUrl,
|
||||
user
|
||||
}: { children?: any; baseUrl?: string; user?: TApiUser | null }) => {
|
||||
export type ClientProviderProps = {
|
||||
children?: any;
|
||||
baseUrl?: string;
|
||||
user?: TApiUser | null | undefined;
|
||||
};
|
||||
|
||||
export const ClientProvider = ({ children, baseUrl, user }: ClientProviderProps) => {
|
||||
const [actualBaseUrl, setActualBaseUrl] = useState<string | null>(null);
|
||||
const winCtx = useBkndWindowContext();
|
||||
|
||||
@@ -87,3 +89,17 @@ export const useBaseUrl = () => {
|
||||
const context = useContext(ClientContext);
|
||||
return context.baseUrl;
|
||||
};
|
||||
|
||||
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"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { BkndProvider, useBknd } from "./BkndProvider";
|
||||
@@ -1,5 +1,10 @@
|
||||
export { ClientProvider, useClient, useBaseUrl } from "./ClientProvider";
|
||||
export { BkndProvider, useBknd } from "./BkndProvider";
|
||||
export {
|
||||
ClientProvider,
|
||||
useBkndWindowContext,
|
||||
type ClientProviderProps,
|
||||
useClient,
|
||||
useBaseUrl
|
||||
} from "./ClientProvider";
|
||||
|
||||
export { useAuth } from "./schema/auth/use-auth";
|
||||
export { Api } from "../../Api";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useBknd } from "ui/client";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
|
||||
export function useBkndAuth() {
|
||||
//const client = useClient();
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
fieldsSchema,
|
||||
relationsSchema
|
||||
} from "data/data-schema";
|
||||
import { useBknd } from "ui/client";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
import type { TSchemaActions } from "ui/client/schema/actions";
|
||||
|
||||
export function useBkndData() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useBknd } from "ui/client";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
|
||||
export function useBkndSystem() {
|
||||
const { config, schema, actions: bkndActions } = useBknd();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useBknd } from "ui";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
|
||||
export function useTheme(): { theme: "light" | "dark" } {
|
||||
const b = useBknd();
|
||||
|
||||
@@ -25,12 +25,6 @@ export const layoutWithDagre = ({ nodes, edges, graph }: LayoutProps) => {
|
||||
const dagreGraph = new Dagre.graphlib.Graph();
|
||||
dagreGraph.setDefaultEdgeLabel(() => ({}));
|
||||
dagreGraph.setGraph(graph || {});
|
||||
/*dagreGraph.setGraph({
|
||||
rankdir: "LR",
|
||||
align: "UR",
|
||||
nodesep: NODE_SEP,
|
||||
ranksep: RANK_SEP
|
||||
});*/
|
||||
|
||||
nodes.forEach((node) => {
|
||||
dagreGraph.setNode(node.id, {
|
||||
@@ -48,7 +42,11 @@ export const layoutWithDagre = ({ nodes, edges, graph }: LayoutProps) => {
|
||||
return {
|
||||
nodes: nodes.map((node) => {
|
||||
const position = dagreGraph.node(node.id);
|
||||
return { ...node, x: position.x, y: position.y };
|
||||
return {
|
||||
...node,
|
||||
x: position.x - (node.width ?? 0) / 2,
|
||||
y: position.y - (node.height ?? 0) / 2
|
||||
};
|
||||
}),
|
||||
edges
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ReactCodeMirrorProps } from "@uiw/react-codemirror";
|
||||
import { Suspense, lazy } from "react";
|
||||
import { useBknd } from "ui/client";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
const CodeMirror = lazy(() => import("@uiw/react-codemirror"));
|
||||
|
||||
export default function CodeEditor({ editable, basicSetup, ...props }: ReactCodeMirrorProps) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export type AlertProps = ComponentPropsWithoutRef<"div"> & {
|
||||
className?: string;
|
||||
visible?: boolean;
|
||||
title?: string;
|
||||
message?: string;
|
||||
message?: ReactNode | string;
|
||||
};
|
||||
|
||||
const Base: React.FC<AlertProps> = ({ visible = true, title, message, className, ...props }) =>
|
||||
|
||||
@@ -12,7 +12,7 @@ import type {
|
||||
ValidatorType
|
||||
} from "@rjsf/utils";
|
||||
import { toErrorSchema } from "@rjsf/utils";
|
||||
import get from "lodash-es/get";
|
||||
import { get } from "lodash-es";
|
||||
|
||||
function removeUndefinedKeys(obj: any): any {
|
||||
if (!obj) return obj;
|
||||
|
||||
@@ -14,9 +14,7 @@ import {
|
||||
getWidget,
|
||||
mergeSchemas
|
||||
} from "@rjsf/utils";
|
||||
import get from "lodash-es/get";
|
||||
import isEmpty from "lodash-es/isEmpty";
|
||||
import omit from "lodash-es/omit";
|
||||
import { get, isEmpty, omit } from "lodash-es";
|
||||
import { Component } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Label } from "../templates/FieldTemplate";
|
||||
|
||||
+1
-11
@@ -1,14 +1,4 @@
|
||||
export { default as Admin } from "./Admin";
|
||||
export { Button } from "./components/buttons/Button";
|
||||
export { Context } from "./components/Context";
|
||||
export {
|
||||
useClient,
|
||||
ClientProvider,
|
||||
BkndProvider,
|
||||
useBknd,
|
||||
useAuth,
|
||||
useBaseUrl
|
||||
} from "./client";
|
||||
export { default as Admin, type BkndAdminProps } from "./Admin";
|
||||
export {
|
||||
EntitiesContainer,
|
||||
useEntities,
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import { Menu, Popover, SegmentedControl, Tooltip } from "@mantine/core";
|
||||
import { SegmentedControl, Tooltip } from "@mantine/core";
|
||||
import { IconKeyOff, IconSettings, IconUser } from "@tabler/icons-react";
|
||||
import {
|
||||
TbDatabase,
|
||||
TbFingerprint,
|
||||
TbHierarchy2,
|
||||
TbMenu2,
|
||||
TbMoon,
|
||||
TbPhoto,
|
||||
TbSelector,
|
||||
TbSun,
|
||||
TbUser,
|
||||
TbX
|
||||
} from "react-icons/tb";
|
||||
import { Button } from "ui";
|
||||
import { useAuth, useBknd } from "ui/client";
|
||||
import { useBkndWindowContext } from "ui/client/BkndProvider";
|
||||
import { useAuth, useBkndWindowContext } from "ui/client";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
import { useBkndSystemTheme } from "ui/client/schema/system/use-bknd-system";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import { IconButton } from "ui/components/buttons/IconButton";
|
||||
import { Logo } from "ui/components/display/Logo";
|
||||
import { Dropdown, type DropdownItem } from "ui/components/overlay/Dropdown";
|
||||
@@ -146,6 +144,7 @@ export function Header({ hasSidebar = true }) {
|
||||
}
|
||||
|
||||
function UserMenu() {
|
||||
const { adminOverride } = useBknd();
|
||||
const auth = useAuth();
|
||||
const [navigate] = useNavigate();
|
||||
const { logout_route } = useBkndWindowContext();
|
||||
@@ -170,7 +169,9 @@ function UserMenu() {
|
||||
items.push({ label: `Logout ${auth.user.email}`, onClick: handleLogout, icon: IconKeyOff });
|
||||
}
|
||||
|
||||
items.push(() => <UserMenuThemeToggler />);
|
||||
if (!adminOverride) {
|
||||
items.push(() => <UserMenuThemeToggler />);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { Button } from "ui";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import {
|
||||
JsonSchemaForm,
|
||||
type JsonSchemaFormProps,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Type } from "core/utils";
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Button } from "ui";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import * as Formy from "ui/components/form/Formy";
|
||||
|
||||
export type LoginFormProps = Omit<ComponentPropsWithoutRef<"form">, "onSubmit"> & {
|
||||
|
||||
@@ -135,10 +135,10 @@ type FormInputElement = HTMLInputElement | HTMLTextAreaElement;
|
||||
function EntityFormField({ fieldApi, field, action, data, ...props }: EntityFormFieldProps) {
|
||||
const handleUpdate = useEvent((e: React.ChangeEvent<FormInputElement> | any) => {
|
||||
if (typeof e === "object" && "target" in e) {
|
||||
console.log("handleUpdate", e.target.value);
|
||||
//console.log("handleUpdate", e.target.value);
|
||||
fieldApi.handleChange(e.target.value);
|
||||
} else {
|
||||
console.log("handleUpdate-", e);
|
||||
//console.log("handleUpdate-", e);
|
||||
fieldApi.handleChange(e);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -69,15 +69,13 @@ export function DataSchemaCanvas() {
|
||||
const nodeLayout = layoutWithDagre({
|
||||
nodes: nodes.map((n) => ({
|
||||
id: n.id,
|
||||
...EntityTableNode.getSize(n)
|
||||
...EntityTableNode.getSize(n.data)
|
||||
})),
|
||||
edges,
|
||||
graph: {
|
||||
rankdir: "LR",
|
||||
//align: "UR",
|
||||
ranker: "network-simplex",
|
||||
nodesep: 350,
|
||||
ranksep: 50
|
||||
marginx: 50,
|
||||
marginy: 50
|
||||
}
|
||||
});
|
||||
|
||||
@@ -88,12 +86,6 @@ export function DataSchemaCanvas() {
|
||||
}
|
||||
});
|
||||
|
||||
/*const _edges = edges.map((e) => ({
|
||||
...e,
|
||||
source: e.source + `-${e.target}_id`,
|
||||
target: e.target + "-id"
|
||||
}));*/
|
||||
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<Canvas
|
||||
|
||||
@@ -4,8 +4,9 @@ import { ucFirst } from "core/utils";
|
||||
import type { EntityData, RelationField } from "data";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { TbEye } from "react-icons/tb";
|
||||
import { Button } from "ui";
|
||||
import { useBknd, useClient } from "ui/client";
|
||||
import { useClient } from "ui/client";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import * as Formy from "ui/components/form/Formy";
|
||||
import { Popover } from "ui/components/overlay/Popover";
|
||||
import { useEntities } from "ui/container";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { typeboxResolver } from "@hookform/resolvers/typebox";
|
||||
import type { Static } from "core/utils";
|
||||
import { type TAppDataEntityFields, entitiesSchema } from "data/data-schema";
|
||||
import { useRef, useState } from "react";
|
||||
import { useRef } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { MantineSelect } from "ui/components/form/hook-form-mantine/MantineSelect";
|
||||
import { useEvent } from "ui/hooks/use-event";
|
||||
@@ -11,31 +12,33 @@ import {
|
||||
import { ModalBody, ModalFooter, type TCreateModalSchema, useStepContext } from "./CreateModal";
|
||||
|
||||
const schema = entitiesSchema;
|
||||
type Schema = Static<typeof schema>;
|
||||
|
||||
export function StepEntityFields() {
|
||||
const { nextStep, stepBack, state, setState } = useStepContext<TCreateModalSchema>();
|
||||
const entity = state.entities?.create?.[0]!;
|
||||
const defaultFields = { id: { type: "primary", name: "id" } } as const;
|
||||
const ref = useRef<EntityFieldsFormRef>(null);
|
||||
const initial = entity
|
||||
? entity
|
||||
: {
|
||||
fields: defaultFields,
|
||||
config: {
|
||||
sort_field: "id",
|
||||
sort_dir: "asc"
|
||||
}
|
||||
};
|
||||
const {
|
||||
control,
|
||||
formState: { isValid, errors },
|
||||
getValues,
|
||||
handleSubmit,
|
||||
watch,
|
||||
register,
|
||||
setValue
|
||||
} = useForm({
|
||||
mode: "onTouched",
|
||||
resolver: typeboxResolver(schema),
|
||||
defaultValues: {
|
||||
...entity,
|
||||
fields: defaultFields,
|
||||
config: {
|
||||
sort_field: "id",
|
||||
sort_dir: "asc"
|
||||
}
|
||||
}
|
||||
defaultValues: initial as NonNullable<Schema>
|
||||
});
|
||||
|
||||
const values = watch();
|
||||
@@ -74,7 +77,11 @@ export function StepEntityFields() {
|
||||
Add fields to <strong>{entity.name}</strong>:
|
||||
</p>
|
||||
<div className="flex flex-col gap-1">
|
||||
<EntityFieldsForm ref={ref} fields={defaultFields} onChange={updateListener} />
|
||||
<EntityFieldsForm
|
||||
ref={ref}
|
||||
fields={initial.fields as any}
|
||||
onChange={updateListener}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
@@ -82,7 +89,7 @@ export function StepEntityFields() {
|
||||
<div className="flex flex-row gap-2">
|
||||
<MantineSelect
|
||||
label="Field"
|
||||
data={Object.keys(values.fields).filter((name) => name.length > 0)}
|
||||
data={Object.keys(values.fields ?? {}).filter((name) => name.length > 0)}
|
||||
placeholder="Select field"
|
||||
name="config.sort_field"
|
||||
allowDeselect={false}
|
||||
@@ -102,7 +109,7 @@ export function StepEntityFields() {
|
||||
<div>
|
||||
{Object.entries(errors).map(([key, value]) => (
|
||||
<p key={key}>
|
||||
{key}: {value.message}
|
||||
{key}: {(value as any).message}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { ManyToOneRelation, type RelationType, RelationTypes } from "data";
|
||||
import { type ReactNode, useEffect } from "react";
|
||||
import { type Control, type FieldValues, type UseFormRegister, useForm } from "react-hook-form";
|
||||
import { useBknd } from "ui/client";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
import { MantineNumberInput } from "ui/components/form/hook-form-mantine/MantineNumberInput";
|
||||
import { MantineSelect } from "ui/components/form/hook-form-mantine/MantineSelect";
|
||||
import { useStepContext } from "ui/components/steps/Steps";
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import {
|
||||
} from "core/utils";
|
||||
import type { MediaFieldConfig } from "media/MediaField";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useBknd } from "ui/client";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
import { MantineNumberInput } from "ui/components/form/hook-form-mantine/MantineNumberInput";
|
||||
import { MantineRadio } from "ui/components/form/hook-form-mantine/MantineRadio";
|
||||
import { MantineSelect } from "ui/components/form/hook-form-mantine/MantineSelect";
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Type } from "core/utils";
|
||||
import { FetchTask } from "flows";
|
||||
import { useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Button } from "ui";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import { JsonViewer } from "ui/components/code/JsonViewer";
|
||||
import { SegmentedControl } from "ui/components/form/SegmentedControl";
|
||||
import { MantineSelect } from "ui/components/form/hook-form-mantine/MantineSelect";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { selectAtom } from "jotai/utils";
|
||||
import { isEqual } from "lodash-es";
|
||||
import type { ModuleSchemas } from "modules/ModuleManager";
|
||||
import { createContext, useCallback, useContext, useEffect } from "react";
|
||||
import { useBknd } from "ui/client";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
|
||||
export type TFlowNodeData = {
|
||||
label: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IconFingerprint } from "@tabler/icons-react";
|
||||
import { TbSettings } from "react-icons/tb";
|
||||
import { useBknd } from "ui/client";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
import { IconButton } from "ui/components/buttons/IconButton";
|
||||
import { Empty } from "ui/components/display/Empty";
|
||||
import { Link } from "ui/components/wouter/Link";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useBknd, useClient } from "ui/client";
|
||||
import { useClient } from "ui/client";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
|
||||
import { Alert } from "ui/components/display/Alert";
|
||||
import { routes } from "ui/lib/routes";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useRef } from "react";
|
||||
import { TbDots } from "react-icons/tb";
|
||||
import { useBknd } from "ui/client";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import { IconButton } from "ui/components/buttons/IconButton";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { cloneDeep, omit } from "lodash-es";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useBknd } from "ui/client";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
|
||||
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { cloneDeep, omit } from "lodash-es";
|
||||
import { useBknd } from "ui/client";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import { JsonSchemaForm } from "ui/components/form/json-schema/JsonSchemaForm";
|
||||
import * as AppShell from "../../layouts/AppShell/AppShell";
|
||||
|
||||
@@ -2,10 +2,10 @@ import { typeboxResolver } from "@hookform/resolvers/typebox";
|
||||
import { Input, Switch, Tooltip } from "@mantine/core";
|
||||
import { guardRoleSchema } from "auth/auth-schema";
|
||||
import { type Static, ucFirst } from "core/utils";
|
||||
import type { TAppDataEntityFields } from "data/data-schema";
|
||||
import { forwardRef, useImperativeHandle } from "react";
|
||||
import { type UseControllerProps, useController, useForm } from "react-hook-form";
|
||||
import { Button, useBknd } from "ui";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import { MantineSwitch } from "ui/components/form/hook-form-mantine/MantineSwitch";
|
||||
|
||||
const schema = guardRoleSchema;
|
||||
|
||||
@@ -2,12 +2,12 @@ import { SegmentedControl } from "@mantine/core";
|
||||
import { IconDatabase } from "@tabler/icons-react";
|
||||
import type { Entity, TEntityType } from "data";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useBknd } from "../../client";
|
||||
import { Empty } from "../../components/display/Empty";
|
||||
import { Link } from "../../components/wouter/Link";
|
||||
import { useBrowserTitle } from "../../hooks/use-browser-title";
|
||||
import * as AppShell from "../../layouts/AppShell/AppShell";
|
||||
import { routes, useNavigate } from "../../lib/routes";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
import { Empty } from "ui/components/display/Empty";
|
||||
import { Link } from "ui/components/wouter/Link";
|
||||
import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
||||
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||
import { routes, useNavigate } from "ui/lib/routes";
|
||||
|
||||
export function DataRoot({ children }) {
|
||||
// @todo: settings routes should be centralized
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
import { encodeSearch, ucFirst } from "core/utils";
|
||||
import type { Entity, EntityData } from "data";
|
||||
import type { EntityRelation } from "data";
|
||||
import { Fragment, memo, useState } from "react";
|
||||
import { TbArrowLeft, TbDots } from "react-icons/tb";
|
||||
import { ucFirst } from "core/utils";
|
||||
import type { Entity, EntityData, EntityRelation } from "data";
|
||||
import { Fragment, useState } from "react";
|
||||
import { TbDots } from "react-icons/tb";
|
||||
import { useClient } from "ui/client";
|
||||
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import { IconButton } from "ui/components/buttons/IconButton";
|
||||
import { Dropdown } from "ui/components/overlay/Dropdown";
|
||||
import { useEntity } from "ui/container";
|
||||
import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
||||
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||
import { Breadcrumbs2 } from "ui/layouts/AppShell/Breadcrumbs2";
|
||||
import { routes, useNavigate } from "ui/lib/routes";
|
||||
import { bkndModals } from "ui/modals";
|
||||
import { EntityForm } from "ui/modules/data/components/EntityForm";
|
||||
import { EntityTable2 } from "ui/modules/data/components/EntityTable2";
|
||||
import { useEntityForm } from "ui/modules/data/hooks/useEntityForm";
|
||||
import { useClient } from "../../client";
|
||||
import { useBknd } from "../../client";
|
||||
import { Button } from "../../components/buttons/Button";
|
||||
import { IconButton } from "../../components/buttons/IconButton";
|
||||
import { Dropdown } from "../../components/overlay/Dropdown";
|
||||
import { useEntity } from "../../container";
|
||||
import { useBrowserTitle } from "../../hooks/use-browser-title";
|
||||
import * as AppShell from "../../layouts/AppShell/AppShell";
|
||||
import { SectionHeaderLink } from "../../layouts/AppShell/AppShell";
|
||||
import { Breadcrumbs2 } from "../../layouts/AppShell/Breadcrumbs2";
|
||||
import { routes, useNavigate } from "../../lib/routes";
|
||||
import { bkndModals } from "../../modals";
|
||||
|
||||
export function DataEntityUpdate({ params }) {
|
||||
const { $data, relations } = useBkndData();
|
||||
|
||||
@@ -2,18 +2,16 @@ import { Type } from "core/utils";
|
||||
import { querySchema } from "data";
|
||||
import { TbDots } from "react-icons/tb";
|
||||
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
|
||||
import { Empty } from "ui/components/display/Empty";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import { IconButton } from "ui/components/buttons/IconButton";
|
||||
import { Message } from "ui/components/display/Message";
|
||||
import { Dropdown } from "ui/components/overlay/Dropdown";
|
||||
import { EntitiesContainer } from "ui/container";
|
||||
import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
||||
import { useSearch } from "ui/hooks/use-search";
|
||||
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||
import { routes, useNavigate } from "ui/lib/routes";
|
||||
import { EntityTable2 } from "ui/modules/data/components/EntityTable2";
|
||||
import { useBknd } from "../../client";
|
||||
import { Button } from "../../components/buttons/Button";
|
||||
import { IconButton } from "../../components/buttons/IconButton";
|
||||
import { Dropdown } from "../../components/overlay/Dropdown";
|
||||
import { EntitiesContainer } from "../../container";
|
||||
import { useBrowserTitle } from "../../hooks/use-browser-title";
|
||||
import { useSearch } from "../../hooks/use-search";
|
||||
import * as AppShell from "../../layouts/AppShell/AppShell";
|
||||
import { routes, useNavigate } from "../../lib/routes";
|
||||
|
||||
// @todo: migrate to Typebox
|
||||
const searchSchema = Type.Composite(
|
||||
|
||||
@@ -18,9 +18,7 @@ import { forwardRef, memo, useEffect, useImperativeHandle } from "react";
|
||||
import { type FieldArrayWithId, type UseFormReturn, useFieldArray, useForm } from "react-hook-form";
|
||||
import { TbGripVertical, TbSettings, TbTrash } from "react-icons/tb";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Button } from "ui";
|
||||
import { useBknd } from "ui/client";
|
||||
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import { IconButton } from "ui/components/buttons/IconButton";
|
||||
import { JsonViewer } from "ui/components/code/JsonViewer";
|
||||
import { MantineSwitch } from "ui/components/form/hook-form-mantine/MantineSwitch";
|
||||
@@ -74,22 +72,16 @@ export const EntityFieldsForm = forwardRef<
|
||||
name,
|
||||
field
|
||||
}));
|
||||
/*const entityFields = entity.fields.map((field) => ({
|
||||
name: field.name,
|
||||
field: field.toJSON()
|
||||
}));*/
|
||||
|
||||
const {
|
||||
control,
|
||||
formState: { isValid, errors },
|
||||
getValues,
|
||||
handleSubmit,
|
||||
watch,
|
||||
register,
|
||||
setValue,
|
||||
setError,
|
||||
reset,
|
||||
clearErrors
|
||||
reset
|
||||
} = useForm({
|
||||
mode: "all",
|
||||
resolver: typeboxResolver(schema),
|
||||
@@ -116,7 +108,6 @@ export const EntityFieldsForm = forwardRef<
|
||||
props?.onChange?.(toCleanValues(data));
|
||||
});
|
||||
}
|
||||
//props?.onChange?.()
|
||||
}, []);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
@@ -124,25 +115,9 @@ export const EntityFieldsForm = forwardRef<
|
||||
getValues: () => getValues(),
|
||||
getData: () => {
|
||||
return toCleanValues(getValues());
|
||||
/*return Object.fromEntries(
|
||||
getValues().fields.map((field) => [field.name, objectCleanEmpty(field.field)])
|
||||
);*/
|
||||
},
|
||||
isValid: () => isValid
|
||||
}));
|
||||
console.log("errors", errors.fields);
|
||||
|
||||
/*useEffect(() => {
|
||||
console.log("change", values);
|
||||
onSubmit(values);
|
||||
}, [values]);*/
|
||||
|
||||
function onSubmit(data: TFieldsFormSchema) {
|
||||
console.log("submit", isValid, data, errors);
|
||||
}
|
||||
function onSubmitInvalid(a, b) {
|
||||
console.log("submit invalid", a, b);
|
||||
}
|
||||
|
||||
function handleAppend(_type: keyof typeof fieldsSchemaObject) {
|
||||
const newField = {
|
||||
@@ -153,7 +128,6 @@ export const EntityFieldsForm = forwardRef<
|
||||
config: {}
|
||||
}
|
||||
};
|
||||
console.log("handleAppend", _type, newField);
|
||||
append(newField);
|
||||
}
|
||||
|
||||
@@ -167,10 +141,7 @@ export const EntityFieldsForm = forwardRef<
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit as any, onSubmitInvalid)}
|
||||
className="flex flex-col gap-6"
|
||||
>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-4">
|
||||
{sortable ? (
|
||||
@@ -222,9 +193,7 @@ export const EntityFieldsForm = forwardRef<
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" className="hidden" />
|
||||
{/*<Debug watch={watch} errors={errors} />*/}
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Suspense, lazy } from "react";
|
||||
import { useBknd } from "ui/client";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
import { Route, Router, Switch } from "wouter";
|
||||
import { AuthLogin } from "./auth/auth.login";
|
||||
import { Root, RootEmpty } from "./root";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { IconFingerprint, IconHome } from "@tabler/icons-react";
|
||||
import { isDebug } from "core";
|
||||
import { Suspense, lazy, useEffect } from "react";
|
||||
import { useAuth } from "ui";
|
||||
import { IconHome } from "@tabler/icons-react";
|
||||
import { Suspense, useEffect } from "react";
|
||||
import { useAuth } from "ui/client";
|
||||
import { Empty } from "../components/display/Empty";
|
||||
import { useBrowserTitle } from "../hooks/use-browser-title";
|
||||
import * as AppShell from "../layouts/AppShell/AppShell";
|
||||
|
||||
@@ -3,23 +3,21 @@ import { type TObject, ucFirst } from "core/utils";
|
||||
import { omit } from "lodash-es";
|
||||
import { type ReactNode, useMemo, useRef, useState } from "react";
|
||||
import { TbSettings } from "react-icons/tb";
|
||||
import { useAuth } from "ui";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import { IconButton } from "ui/components/buttons/IconButton";
|
||||
import { Alert } from "ui/components/display/Alert";
|
||||
import { Link, Route, useLocation } from "wouter";
|
||||
import { useBknd } from "../../../client/BkndProvider";
|
||||
import { Button } from "../../../components/buttons/Button";
|
||||
import { IconButton } from "../../../components/buttons/IconButton";
|
||||
import { Empty } from "../../../components/display/Empty";
|
||||
import { Empty } from "ui/components/display/Empty";
|
||||
import {
|
||||
JsonSchemaForm,
|
||||
type JsonSchemaFormRef
|
||||
} from "../../../components/form/json-schema/JsonSchemaForm";
|
||||
import { Dropdown } from "../../../components/overlay/Dropdown";
|
||||
import { DataTable } from "../../../components/table/DataTable";
|
||||
import { useEvent } from "../../../hooks/use-event";
|
||||
import * as AppShell from "../../../layouts/AppShell/AppShell";
|
||||
import { SectionHeaderTabs } from "../../../layouts/AppShell/AppShell";
|
||||
import { Breadcrumbs } from "../../../layouts/AppShell/Breadcrumbs";
|
||||
} from "ui/components/form/json-schema/JsonSchemaForm";
|
||||
import { Dropdown } from "ui/components/overlay/Dropdown";
|
||||
import { DataTable } from "ui/components/table/DataTable";
|
||||
import { useEvent } from "ui/hooks/use-event";
|
||||
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||
import { Breadcrumbs } from "ui/layouts/AppShell/Breadcrumbs";
|
||||
import { Link, Route, useLocation } from "wouter";
|
||||
import { extractSchema } from "../utils/schema";
|
||||
import { SettingNewModal, type SettingsNewModalProps } from "./SettingNewModal";
|
||||
import { SettingSchemaModal, type SettingsSchemaModalRef } from "./SettingSchemaModal";
|
||||
@@ -234,7 +232,7 @@ export function Setting<Schema extends TObject = any>({
|
||||
<Breadcrumbs path={path} />
|
||||
</AppShell.SectionHeader>
|
||||
<AppShell.Scrollable key={path.join("-")}>
|
||||
{typeof showAlert === "string" && <Alert.Warning message={showAlert} />}
|
||||
{typeof showAlert !== "undefined" && <Alert.Warning message={showAlert} />}
|
||||
|
||||
<div className="flex flex-col flex-grow p-3 gap-3">
|
||||
<JsonSchemaForm
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { modals } from "@mantine/modals";
|
||||
import { IconSettings } from "@tabler/icons-react";
|
||||
import { ucFirst } from "core/utils";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
import { Empty } from "ui/components/display/Empty";
|
||||
import { Link } from "ui/components/wouter/Link";
|
||||
import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
||||
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||
import { Route, Switch } from "wouter";
|
||||
import { useBknd } from "../../client";
|
||||
import { Empty } from "../../components/display/Empty";
|
||||
import { Link } from "../../components/wouter/Link";
|
||||
import { useBrowserTitle } from "../../hooks/use-browser-title";
|
||||
import * as AppShell from "../../layouts/AppShell/AppShell";
|
||||
import { bkndModals } from "../../modals";
|
||||
import { Setting } from "./components/Setting";
|
||||
import { AuthSettings } from "./routes/auth.settings";
|
||||
import { DataSettings } from "./routes/data.settings";
|
||||
import { FlowsSettings } from "./routes/flows.settings";
|
||||
import { ServerSettings } from "./routes/server.settings";
|
||||
|
||||
function SettingsSidebar() {
|
||||
const { version, schema } = useBknd();
|
||||
@@ -39,36 +38,6 @@ function SettingsSidebar() {
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
{/*<button
|
||||
onClick={() =>
|
||||
modals.openContextModal({
|
||||
modal: "test",
|
||||
title: "Test Modal",
|
||||
innerProps: { modalBody: "This is a test modal" }
|
||||
})
|
||||
}
|
||||
>
|
||||
modal
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
bkndModals.open(bkndModals.ids.test, { modalBody: "test" }, { title: "what" })
|
||||
}
|
||||
>
|
||||
modal2
|
||||
</button>
|
||||
<button onClick={() => bkndModals.open("test", { modalBody: "test" })}>modal</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
bkndModals.open("debug", {
|
||||
data: {
|
||||
one: { what: 1 }
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
debug
|
||||
</button>*/}
|
||||
</AppShell.Scrollable>
|
||||
</AppShell.Sidebar>
|
||||
);
|
||||
@@ -145,12 +114,7 @@ const SettingRoutesRoutes = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<FallbackRoutes
|
||||
module="server"
|
||||
schema={schema}
|
||||
config={config}
|
||||
uiSchema={uiSchema.server}
|
||||
/>
|
||||
<ServerSettings schema={schema.server} config={config.server} />
|
||||
<DataSettings schema={schema.data} config={config.data} />
|
||||
<AuthSettings schema={schema.auth} config={config.auth} />
|
||||
<FallbackRoutes module="media" schema={schema} config={config} uiSchema={uiSchema.media} />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { cloneDeep, transform } from "lodash-es";
|
||||
import type { ModuleConfigs, ModuleSchemas } from "modules";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
import { fieldSpecs } from "ui/modules/data/components/fields-specs";
|
||||
import { Route, Switch } from "wouter";
|
||||
import type { ModuleConfigs, ModuleSchemas } from "../../../../modules";
|
||||
import { useBknd } from "../../../client";
|
||||
import { Setting } from "../components/Setting";
|
||||
|
||||
export const dataFieldsUiSchema = {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
import { Setting } from "ui/routes/settings/components/Setting";
|
||||
import { Route } from "wouter";
|
||||
|
||||
const uiSchema = {
|
||||
cors: {
|
||||
allow_methods: {
|
||||
"ui:widget": "checkboxes"
|
||||
},
|
||||
allow_headers: {
|
||||
"ui:options": {
|
||||
orderable: false
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const ServerSettings = ({ schema: _unsafe_copy, config }) => {
|
||||
const { app, adminOverride } = useBknd();
|
||||
const { basepath } = app.getAdminConfig();
|
||||
const _schema = cloneDeep(_unsafe_copy);
|
||||
const prefix = `~/${basepath}/settings`.replace(/\/+/g, "/");
|
||||
|
||||
const schema = _schema;
|
||||
if (adminOverride) {
|
||||
schema.properties.admin.readOnly = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<Route path="/server" nest>
|
||||
<Route
|
||||
path="/"
|
||||
component={() => (
|
||||
<Setting
|
||||
options={{
|
||||
showAlert: () => {
|
||||
if (adminOverride) {
|
||||
return "The admin settings are read-only as they are overriden. Remaining server configuration can be edited.";
|
||||
}
|
||||
return;
|
||||
}
|
||||
}}
|
||||
schema={schema}
|
||||
uiSchema={uiSchema}
|
||||
config={config}
|
||||
prefix={`${prefix}/server`}
|
||||
path={["server"]}
|
||||
/>
|
||||
)}
|
||||
nest
|
||||
/>
|
||||
</Route>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
# Stage 1: Build stage
|
||||
FROM node:20 as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install & copy required cli
|
||||
RUN npm install --omit=dev bknd
|
||||
RUN mkdir /output && cp -r node_modules/bknd/dist /output/dist
|
||||
|
||||
# Stage 2: Final minimal image
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install pm2 and libsql
|
||||
RUN npm install -g pm2
|
||||
RUN echo '{"type":"module"}' > package.json
|
||||
RUN npm install @libsql/client
|
||||
|
||||
# Create volume and init args
|
||||
VOLUME /data
|
||||
ENV DEFAULT_ARGS "--db-url file:/data/data.db"
|
||||
|
||||
# Copy output from builder
|
||||
COPY --from=builder /output/dist ./dist
|
||||
|
||||
EXPOSE 1337
|
||||
CMD ["pm2-runtime", "dist/cli/index.js run ${ARGS:-${DEFAULT_ARGS}}"]
|
||||
@@ -0,0 +1,31 @@
|
||||
# Official `bknd` Docker image
|
||||
The docker image intentially doesn't copy any data into the image for now, so you can copy the Dockerfile and build the image anywhere.
|
||||
|
||||
## Building the Docker image
|
||||
To build the Docker image, run the following command:
|
||||
|
||||
```bash
|
||||
docker build -t bknd .
|
||||
```
|
||||
|
||||
## Running the Docker container
|
||||
To run the Docker container, run the following command:
|
||||
|
||||
```bash
|
||||
docker run -p 1337:1337 bknd
|
||||
```
|
||||
|
||||
You can pass the same CLI arguments (see [Using the CLI](https://docs.bknd.io/cli) guide) to the docker container as you'd do with `npx bknd run`, like so:
|
||||
|
||||
```bash
|
||||
docker run -p 1337:1337 -e ARGS="--db-url file:/data/data.db" bknd
|
||||
```
|
||||
Or connect to a remote turso database:
|
||||
```bash
|
||||
docker run -p 1337:1337 -e ARGS="--db-url libsql://<db>.turso.io --db-token <token>" bknd
|
||||
```
|
||||
|
||||
To mount the data directory to the host, you can use the `-v` flag:
|
||||
```bash
|
||||
docker run -p 1337:1337 -v /path/to/data:/data bknd
|
||||
```
|
||||
@@ -0,0 +1,124 @@
|
||||
---
|
||||
title: 'Astro'
|
||||
description: 'Run bknd inside Astro'
|
||||
---
|
||||
import InstallBknd from '/snippets/install-bknd.mdx';
|
||||
|
||||
## Installation
|
||||
Install bknd as a dependency:
|
||||
<InstallBknd />
|
||||
|
||||
<Note>The guide below assumes you're using Astro v4. We've experienced issues with Astro DB
|
||||
using v5, see [this issue](https://github.com/withastro/astro/issues/12474).</Note>
|
||||
|
||||
For the Astro integration to work, you also need to [add the react integration](https://docs.astro.build/en/guides/integrations-guide/react/):
|
||||
```bash
|
||||
npx astro add react
|
||||
```
|
||||
|
||||
You also need to make sure to set the output to `hybrid` in your Astro config:
|
||||
```js {6}
|
||||
// astro.config.mjs
|
||||
import { defineConfig } from "astro/config";
|
||||
import react from "@astrojs/react";
|
||||
|
||||
export default defineConfig({
|
||||
output: "hybrid",
|
||||
integrations: [react()]
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
If you don't want to use React with Astro, there is also an option to serve the bknd Admin UI
|
||||
statically using Astro's middleware. In case you're interested in this, feel free to reach
|
||||
out in [Discord](https://discord.gg/952SFk8Tb8) or open an [issue on GitHub](https://github.com/bknd-io/bknd/issues/new).
|
||||
</Note>
|
||||
|
||||
## Serve the API
|
||||
Create a new catch-all route at `src/pages/api/[...api].ts`:
|
||||
```ts src/pages/api/[...api].ts
|
||||
import { serve } from "bknd/adapter/astro";
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
export const ALL = serve({
|
||||
connection: {
|
||||
type: "libsql",
|
||||
config: {
|
||||
url: "file:data.db"
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
For more information about the connection object, refer to the [Setup](/setup) guide. In the
|
||||
special case of astro, you may also use your Astro DB credentials since it's also using LibSQL
|
||||
under the hood. Refer to the [Astro DB documentation](https://docs.astro.build/en/guides/astro-db/) for more information.
|
||||
|
||||
## Enabling the Admin UI
|
||||
Create a new catch-all route at `src/pages/admin/[...admin].astro`:
|
||||
```jsx src/pages/admin/[...admin].astro
|
||||
---
|
||||
import { Admin } from "bknd/ui";
|
||||
import "bknd/dist/styles.css";
|
||||
|
||||
import { getApi } from "bknd/adapter/astro";
|
||||
|
||||
const api = getApi(Astro, { mode: "dynamic" });
|
||||
const user = api.getUser();
|
||||
|
||||
export const prerender = false;
|
||||
---
|
||||
|
||||
<html>
|
||||
<body>
|
||||
<Admin
|
||||
withProvider={{ user }}
|
||||
config={{ basepath: "/admin", color_scheme: "dark" }}
|
||||
client:only
|
||||
/>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## Example usage of the API
|
||||
You use the API in both static and SSR pages. Just note that on static pages, authentication
|
||||
might not work as expected, because Cookies are not available in the static context.
|
||||
|
||||
Here is an example of using the API in static context:
|
||||
```jsx
|
||||
---
|
||||
import { getApi } from "bknd/adapter/astro";
|
||||
const api = getApi(Astro);
|
||||
const { data } = await api.data.readMany("todos");
|
||||
---
|
||||
|
||||
<ul>
|
||||
{data.map((todo) => (
|
||||
<li>{todo.title}</li>
|
||||
))}
|
||||
</ul>
|
||||
```
|
||||
|
||||
On SSR pages, you can also access the authenticated user:
|
||||
```jsx
|
||||
---
|
||||
import { getApi } from "bknd/adapter/astro";
|
||||
const api = getApi(Astro, { mode: "dynamic" });
|
||||
const user = api.getUser();
|
||||
const { data } = await api.data.readMany("todos");
|
||||
|
||||
export const prerender = false;
|
||||
---
|
||||
|
||||
{user
|
||||
? <p>Logged in as <b>{user.email}</b>.</p>
|
||||
: <p>Not authenticated.</p>}
|
||||
<ul>
|
||||
{data.map((todo) => (
|
||||
<li>{todo.title}</li>
|
||||
))}
|
||||
</ul>
|
||||
```
|
||||
|
||||
Check the [astro repository example](https://github.com/bknd-io/bknd/tree/main/examples/astro)
|
||||
for more implementation details or a [fully working example using Astro DB](https://github.com/dswbx/bknd-astro-example).
|
||||
@@ -16,7 +16,8 @@ the admin panel.
|
||||
// index.ts
|
||||
import { serve } from "bknd/adapter/bun";
|
||||
|
||||
const handler = serve({
|
||||
// if the configuration is omitted, it uses an in-memory database
|
||||
serve({
|
||||
connection: {
|
||||
type: "libsql",
|
||||
config: {
|
||||
@@ -25,13 +26,6 @@ const handler = serve({
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Bun.serve({
|
||||
port: 1337,
|
||||
fetch: handler
|
||||
});
|
||||
|
||||
console.log("Server running at http://localhost:1337");
|
||||
```
|
||||
For more information about the connection object, refer to the [Setup](/setup) guide.
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
title: 'Docker'
|
||||
description: 'Official docker image for bknd'
|
||||
---
|
||||
|
||||
# Official `bknd` Docker image
|
||||
The docker image intentially doesn't copy any data into the image for now, so you can copy the
|
||||
Dockerfile and build the image anywhere.
|
||||
|
||||
Locate the Dockerfile either by pulling the [repository](https://github.com/bknd-io/bknd) and navigating to the `docker`
|
||||
directory,
|
||||
or download from [here](https://github.com/bknd-io/bknd/blob/main/docker/Dockerfile).
|
||||
|
||||
## Building the Docker image
|
||||
To build the Docker image, run the following command:
|
||||
|
||||
```bash
|
||||
docker build -t bknd .
|
||||
```
|
||||
|
||||
## Running the Docker container
|
||||
To run the Docker container, run the following command:
|
||||
|
||||
```bash
|
||||
docker run -p 1337:1337 bknd
|
||||
```
|
||||
|
||||
You can pass the same CLI arguments (see [Using the CLI](https://docs.bknd.io/cli) guide) to the
|
||||
docker container as you'd do with
|
||||
`npx bknd
|
||||
run`,
|
||||
like so:
|
||||
|
||||
```bash
|
||||
docker run -p 1337:1337 -e ARGS="--db-url file:/data/data.db" bknd
|
||||
```
|
||||
Or connect to a remote turso database:
|
||||
```bash
|
||||
docker run -p 1337:1337 -e ARGS="--db-url libsql://<db>.turso.io --db-token <token>" bknd
|
||||
```
|
||||
|
||||
To mount the data directory to the host, you can use the `-v` flag:
|
||||
```bash
|
||||
docker run -p 1337:1337 -v /path/to/data:/data bknd
|
||||
```
|
||||
@@ -34,11 +34,26 @@ For more information about the connection object, refer to the [Setup](/setup) g
|
||||
Create a file `[[...admin]].tsx` inside the `pages/admin` folder:
|
||||
```tsx
|
||||
// pages/admin/[[...admin]].tsx
|
||||
import { adminPage, getServerSideProps } from "bknd/adapter/nextjs";
|
||||
import { withApi } from "bknd/adapter/nextjs";
|
||||
import dynamic from "next/dynamic";
|
||||
import "bknd/dist/styles.css";
|
||||
|
||||
export { getServerSideProps };
|
||||
export default adminPage();
|
||||
const Admin = dynamic(() => import("bknd/ui").then((mod) => mod.Admin), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export const getServerSideProps = withApi(async (context) => {
|
||||
return {
|
||||
props: {
|
||||
user: context.api.getUser(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default function AdminPage() {
|
||||
if (typeof document === "undefined") return null;
|
||||
return <Admin withProvider config={{ basepath: "/admin" }} />;
|
||||
}
|
||||
```
|
||||
|
||||
## Example usage of the API in pages dir
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: 'Node'
|
||||
description: 'Run bknd inside Node'
|
||||
---
|
||||
import InstallBknd from '/snippets/install-bknd.mdx';
|
||||
|
||||
## Installation
|
||||
Install bknd as a dependency:
|
||||
<InstallBknd />
|
||||
|
||||
## Serve the API & static files
|
||||
The `serve` function of the Node adapter makes sure to also serve the static files required for
|
||||
the admin panel.
|
||||
|
||||
``` tsx
|
||||
// index.js
|
||||
import { serve } from "bknd/adapter/node";
|
||||
|
||||
// if the configuration is omitted, it uses an in-memory database
|
||||
/** @type {import("bknd/adapter/node").NodeAdapterOptions} */
|
||||
const config = {
|
||||
connection: {
|
||||
type: "libsql",
|
||||
config: {
|
||||
url: ":memory:"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
serve(config);
|
||||
```
|
||||
For more information about the connection object, refer to the [Setup](/setup) guide.
|
||||
|
||||
Run the application using node by executing:
|
||||
```bash
|
||||
node index.js
|
||||
```
|
||||
@@ -81,7 +81,9 @@ Create a new splat route file at `app/routes/admin.$.tsx`:
|
||||
import { adminPage } from "bknd/adapter/remix";
|
||||
import "bknd/dist/styles.css";
|
||||
|
||||
export default adminPage();
|
||||
export default adminPage({
|
||||
config: { basepath: "/admin" }
|
||||
});
|
||||
```
|
||||
|
||||
## Example usage of the API
|
||||
|
||||
@@ -46,6 +46,15 @@ in the future, so stay tuned!
|
||||
</div>}
|
||||
href="/integration/remix"
|
||||
/>
|
||||
<Card
|
||||
title="Astro"
|
||||
icon={<div className="text-primary-light">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<rect width="24" height="24" fill="none"/><path fill="currentColor" d="M9.24 19.035c-.901-.826-1.164-2.561-.789-3.819c.65.793 1.552 1.044 2.486 1.186c1.44.218 2.856.137 4.195-.524c.153-.076.295-.177.462-.278c.126.365.159.734.115 1.11c-.107.915-.56 1.622-1.283 2.158c-.289.215-.594.406-.892.608c-.916.622-1.164 1.35-.82 2.41l.034.114a2.4 2.4 0 0 1-1.07-.918a2.6 2.6 0 0 1-.412-1.401c-.003-.248-.003-.497-.036-.74c-.081-.595-.36-.86-.883-.876a1.034 1.034 0 0 0-1.075.843q-.013.058-.033.126M4.1 15.007s2.666-1.303 5.34-1.303l2.016-6.26c.075-.304.296-.51.544-.51c.25 0 .47.206.545.51l2.016 6.26c3.167 0 5.34 1.303 5.34 1.303L15.363 2.602c-.13-.366-.35-.602-.645-.602H9.283c-.296 0-.506.236-.645.602c-.01.024-4.538 12.405-4.538 12.405"/>
|
||||
</svg>
|
||||
</div>}
|
||||
href="/integration/astro"
|
||||
/>
|
||||
<Card
|
||||
title="Cloudflare"
|
||||
icon={<div className="text-primary-light">
|
||||
@@ -64,4 +73,21 @@ in the future, so stay tuned!
|
||||
</div>}
|
||||
href="/integration/bun"
|
||||
/>
|
||||
<Card
|
||||
title="Node"
|
||||
icon={<div className="text-primary-light">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={24} height={24} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12 23.956c-.342 0-.66-.089-.957-.243l-3.029-1.738c-.455-.242-.227-.33-.09-.374c.614-.198.728-.242 1.366-.595c.068-.044.16-.022.228.022l2.323 1.343c.09.044.205.044.273 0l9.087-5.084c.09-.044.136-.132.136-.242V6.899c0-.11-.045-.198-.136-.242l-9.087-5.061c-.091-.044-.205-.044-.273 0L2.754 6.657c-.091.044-.137.154-.137.242v10.146c0 .088.046.198.137.242l2.482 1.387c1.344.66 2.186-.11 2.186-.88V7.78c0-.132.114-.264.274-.264h1.161c.137 0 .273.11.273.264v10.013c0 1.739-.979 2.751-2.687 2.751c-.524 0-.934 0-2.095-.55l-2.391-1.32A1.85 1.85 0 0 1 1 17.067V6.921c0-.66.364-1.276.957-1.606L11.044.23a2.1 2.1 0 0 1 1.912 0l9.088 5.084c.592.33.956.946.956 1.606v10.146c0 .66-.364 1.276-.956 1.607l-9.087 5.083a2.4 2.4 0 0 1-.957.198m2.801-6.977c-3.985 0-4.805-1.76-4.805-3.257c0-.132.114-.264.273-.264h1.184c.137 0 .25.088.25.22c.183 1.166.707 1.738 3.121 1.738c1.913 0 2.733-.418 2.733-1.408c0-.572-.228-.99-3.211-1.276c-2.483-.243-4.031-.77-4.031-2.685c0-1.783 1.548-2.84 4.145-2.84c2.915 0 4.35.969 4.532 3.082a.35.35 0 0 1-.069.198c-.045.044-.113.088-.182.088h-1.184a.265.265 0 0 1-.25-.198c-.274-1.21-.98-1.607-2.847-1.607c-2.096 0-2.346.704-2.346 1.233c0 .638.296.836 3.12 1.188c2.801.352 4.122.858 4.122 2.75c-.023 1.938-1.662 3.038-4.555 3.038"></path>
|
||||
</svg>
|
||||
</div>}
|
||||
href="/integration/node"
|
||||
/>
|
||||
<Card
|
||||
title="Docker"
|
||||
icon={<div className="text-primary-light">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={30} height={30} viewBox="0 0 24 24"><path
|
||||
fill="currentColor" d="M21.81 10.25c-.06-.04-.56-.43-1.64-.43c-.28 0-.56.03-.84.08c-.21-1.4-1.38-2.11-1.43-2.14l-.29-.17l-.18.27c-.24.36-.43.77-.51 1.19c-.2.8-.08 1.56.33 2.21c-.49.28-1.29.35-1.46.35H2.62c-.34 0-.62.28-.62.63c0 1.15.18 2.3.58 3.38c.45 1.19 1.13 2.07 2 2.61c.98.6 2.59.94 4.42.94c.79 0 1.61-.07 2.42-.22c1.12-.2 2.2-.59 3.19-1.16A8.3 8.3 0 0 0 16.78 16c1.05-1.17 1.67-2.5 2.12-3.65h.19c1.14 0 1.85-.46 2.24-.85c.26-.24.45-.53.59-.87l.08-.24zm-17.96.99h1.76c.08 0 .16-.07.16-.16V9.5c0-.08-.07-.16-.16-.16H3.85c-.09 0-.16.07-.16.16v1.58c.01.09.07.16.16.16m2.43 0h1.76c.08 0 .16-.07.16-.16V9.5c0-.08-.07-.16-.16-.16H6.28c-.09 0-.16.07-.16.16v1.58c.01.09.07.16.16.16m2.47 0h1.75c.1 0 .17-.07.17-.16V9.5c0-.08-.06-.16-.17-.16H8.75c-.08 0-.15.07-.15.16v1.58c0 .09.06.16.15.16m2.44 0h1.77c.08 0 .15-.07.15-.16V9.5c0-.08-.06-.16-.15-.16h-1.77c-.08 0-.15.07-.15.16v1.58c0 .09.07.16.15.16M6.28 9h1.76c.08 0 .16-.09.16-.18V7.25c0-.09-.07-.16-.16-.16H6.28c-.09 0-.16.06-.16.16v1.57c.01.09.07.18.16.18m2.47 0h1.75c.1 0 .17-.09.17-.18V7.25c0-.09-.06-.16-.17-.16H8.75c-.08 0-.15.06-.15.16v1.57c0 .09.06.18.15.18m2.44 0h1.77c.08 0 .15-.09.15-.18V7.25c0-.09-.07-.16-.15-.16h-1.77c-.08 0-.15.06-.15.16v1.57c0 .09.07.18.15.18m0-2.28h1.77c.08 0 .15-.07.15-.16V5c0-.1-.07-.17-.15-.17h-1.77c-.08 0-.15.06-.15.17v1.56c0 .08.07.16.15.16m2.46 4.52h1.76c.09 0 .16-.07.16-.16V9.5c0-.08-.07-.16-.16-.16h-1.76c-.08 0-.15.07-.15.16v1.58c0 .09.07.16.15.16"></path></svg>
|
||||
</div>}
|
||||
href="/integration/docker"
|
||||
/>
|
||||
</CardGroup>
|
||||
|
||||
+8
-18
@@ -3,7 +3,8 @@
|
||||
"name": "bknd",
|
||||
"logo": {
|
||||
"dark": "/_assets/logo/bknd_logo_white.svg",
|
||||
"light": "/_assets/logo/bknd_logo_black.svg"
|
||||
"light": "/_assets/logo/bknd_logo_black.svg",
|
||||
"href": "https://bknd.io"
|
||||
},
|
||||
"theme": "prism",
|
||||
"layout": "sidenav",
|
||||
@@ -22,8 +23,8 @@
|
||||
},
|
||||
"topbarLinks": [
|
||||
{
|
||||
"name": "Support",
|
||||
"url": "mailto:hi@help.bknd.io"
|
||||
"name": "Discord",
|
||||
"url": "https://discord.gg/952SFk8Tb8"
|
||||
}
|
||||
],
|
||||
"topbarCtaButton": {
|
||||
@@ -44,24 +45,12 @@
|
||||
}
|
||||
],
|
||||
"anchors": [
|
||||
{
|
||||
"name": "Web",
|
||||
"icon": "globe",
|
||||
"iconType": "solid",
|
||||
"url": "https://bknd.io"
|
||||
},
|
||||
{
|
||||
"name": "Docs",
|
||||
"icon": "book-open-cover",
|
||||
"iconType": "solid",
|
||||
"url": "/"
|
||||
},
|
||||
{
|
||||
"name": "Discord",
|
||||
"icon": "discord",
|
||||
"iconType": "solid",
|
||||
"url": "https://discord.gg/YAHnKXr5"
|
||||
},
|
||||
{
|
||||
"name": "GitHub",
|
||||
"icon": "github",
|
||||
@@ -114,9 +103,11 @@
|
||||
"integration/bun",
|
||||
"integration/vite",
|
||||
"integration/express",
|
||||
"integration/nodejs",
|
||||
"integration/astro",
|
||||
"integration/node",
|
||||
"integration/deno",
|
||||
"integration/browser"
|
||||
"integration/browser",
|
||||
"integration/docker"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -166,7 +157,6 @@
|
||||
"guide/media/transformations"
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"group": "API Documentation",
|
||||
"pages": ["api-reference/introduction"]
|
||||
|
||||
+1
-1
@@ -5,6 +5,6 @@
|
||||
"dev": "mintlify dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mintlify": "^4.0.269"
|
||||
"mintlify": "^4.0.285"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user