mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +00:00
@@ -1,4 +1,4 @@
|
|||||||
import { describe, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import type { TObject, TString } from "@sinclair/typebox";
|
import type { TObject, TString } from "@sinclair/typebox";
|
||||||
import { Registry } from "../../src/core/registry/Registry";
|
import { Registry } from "../../src/core/registry/Registry";
|
||||||
import { type TSchema, Type } from "../../src/core/utils";
|
import { type TSchema, Type } from "../../src/core/utils";
|
||||||
@@ -11,6 +11,9 @@ class What {
|
|||||||
method() {
|
method() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
getType() {
|
||||||
|
return Type.Object({ type: Type.String() });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
class What2 extends What {}
|
class What2 extends What {}
|
||||||
class NotAllowed {}
|
class NotAllowed {}
|
||||||
@@ -32,25 +35,53 @@ describe("Registry", () => {
|
|||||||
} satisfies Record<string, Test1>);
|
} satisfies Record<string, Test1>);
|
||||||
|
|
||||||
const item = registry.get("first");
|
const item = registry.get("first");
|
||||||
|
expect(item).toBeDefined();
|
||||||
|
expect(item?.cls).toBe(What);
|
||||||
|
|
||||||
|
const second = Type.Object({ type: Type.String(), what: Type.String() });
|
||||||
registry.add("second", {
|
registry.add("second", {
|
||||||
cls: What2,
|
cls: What2,
|
||||||
schema: Type.Object({ type: Type.String(), what: Type.String() }),
|
schema: second,
|
||||||
enabled: true
|
enabled: true
|
||||||
});
|
});
|
||||||
|
// @ts-ignore
|
||||||
|
expect(registry.get("second").schema).toEqual(second);
|
||||||
|
|
||||||
|
const third = Type.Object({ type: Type.String({ default: "1" }), what22: Type.String() });
|
||||||
registry.add("third", {
|
registry.add("third", {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
cls: NotAllowed,
|
cls: NotAllowed,
|
||||||
schema: Type.Object({ type: Type.String({ default: "1" }), what22: Type.String() }),
|
schema: third,
|
||||||
enabled: true
|
enabled: true
|
||||||
});
|
});
|
||||||
|
// @ts-ignore
|
||||||
|
expect(registry.get("third").schema).toEqual(third);
|
||||||
|
|
||||||
|
const fourth = Type.Object({ type: Type.Number(), what22: Type.String() });
|
||||||
registry.add("fourth", {
|
registry.add("fourth", {
|
||||||
cls: What,
|
cls: What,
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
schema: Type.Object({ type: Type.Number(), what22: Type.String() }),
|
schema: fourth,
|
||||||
enabled: true
|
enabled: true
|
||||||
});
|
});
|
||||||
|
// @ts-ignore
|
||||||
|
expect(registry.get("fourth").schema).toEqual(fourth);
|
||||||
|
|
||||||
console.log("list", registry.all());
|
expect(Object.keys(registry.all()).length).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses registration fn", async () => {
|
||||||
|
const registry = new Registry<Test1>((a: ClassRef<What>) => {
|
||||||
|
return {
|
||||||
|
cls: a,
|
||||||
|
schema: a.prototype.getType(),
|
||||||
|
enabled: true
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.register("what2", What2);
|
||||||
|
expect(registry.get("what2")).toBeDefined();
|
||||||
|
expect(registry.get("what2").cls).toBe(What2);
|
||||||
|
expect(registry.get("what2").schema).toEqual(What2.prototype.getType());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ describe("Mutator simple", async () => {
|
|||||||
new TextField("label", { required: true, minLength: 1 }),
|
new TextField("label", { required: true, minLength: 1 }),
|
||||||
new NumberField("count", { default_value: 0 })
|
new NumberField("count", { default_value: 0 })
|
||||||
]);
|
]);
|
||||||
const em = new EntityManager([items], connection);
|
const em = new EntityManager<any>([items], connection);
|
||||||
|
|
||||||
await em.connection.kysely.schema
|
await em.connection.kysely.schema
|
||||||
.createTable("items")
|
.createTable("items")
|
||||||
@@ -175,4 +175,18 @@ describe("Mutator simple", async () => {
|
|||||||
{ id: 8, label: "keep", count: 0 }
|
{ id: 8, label: "keep", count: 0 }
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("insertMany", async () => {
|
||||||
|
const oldCount = (await em.repo(items).count()).count;
|
||||||
|
const inserts = [{ label: "insert 1" }, { label: "insert 2" }];
|
||||||
|
const { data } = await em.mutator(items).insertMany(inserts);
|
||||||
|
|
||||||
|
expect(data.length).toBe(2);
|
||||||
|
expect(data.map((d) => ({ label: d.label }))).toEqual(inserts);
|
||||||
|
const newCount = (await em.repo(items).count()).count;
|
||||||
|
expect(newCount).toBe(oldCount + inserts.length);
|
||||||
|
|
||||||
|
const { data: data2 } = await em.repo(items).findMany({ offset: oldCount });
|
||||||
|
expect(data2).toEqual(data);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import {
|
|||||||
BooleanField,
|
BooleanField,
|
||||||
DateField,
|
DateField,
|
||||||
Entity,
|
Entity,
|
||||||
|
EntityIndex,
|
||||||
|
EntityManager,
|
||||||
EnumField,
|
EnumField,
|
||||||
JsonField,
|
JsonField,
|
||||||
ManyToManyRelation,
|
ManyToManyRelation,
|
||||||
@@ -12,6 +14,7 @@ import {
|
|||||||
PolymorphicRelation,
|
PolymorphicRelation,
|
||||||
TextField
|
TextField
|
||||||
} from "../../src/data";
|
} from "../../src/data";
|
||||||
|
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
||||||
import {
|
import {
|
||||||
FieldPrototype,
|
FieldPrototype,
|
||||||
type FieldSchema,
|
type FieldSchema,
|
||||||
@@ -20,6 +23,7 @@ import {
|
|||||||
boolean,
|
boolean,
|
||||||
date,
|
date,
|
||||||
datetime,
|
datetime,
|
||||||
|
em,
|
||||||
entity,
|
entity,
|
||||||
enumm,
|
enumm,
|
||||||
json,
|
json,
|
||||||
@@ -46,12 +50,17 @@ describe("prototype", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("...2", async () => {
|
test("...2", async () => {
|
||||||
const user = entity("users", {
|
const users = entity("users", {
|
||||||
name: text().required(),
|
name: text(),
|
||||||
bio: text(),
|
bio: text(),
|
||||||
age: number(),
|
age: number(),
|
||||||
some: number().required()
|
some: number()
|
||||||
});
|
});
|
||||||
|
type db = {
|
||||||
|
users: Schema<typeof users>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const obj: Schema<typeof users> = {} as any;
|
||||||
|
|
||||||
//console.log("user", user.toJSON());
|
//console.log("user", user.toJSON());
|
||||||
});
|
});
|
||||||
@@ -266,4 +275,38 @@ describe("prototype", () => {
|
|||||||
|
|
||||||
const obj: Schema<typeof test> = {} as any;
|
const obj: Schema<typeof test> = {} as any;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("schema", async () => {
|
||||||
|
const _em = em(
|
||||||
|
{
|
||||||
|
posts: entity("posts", { name: text(), slug: text().required() }),
|
||||||
|
comments: entity("comments", { some: text() }),
|
||||||
|
users: entity("users", { email: text() })
|
||||||
|
},
|
||||||
|
({ relation, index }, { posts, comments, users }) => {
|
||||||
|
relation(posts).manyToOne(comments).manyToOne(users);
|
||||||
|
index(posts).on(["name"]).on(["slug"], true);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
type LocalDb = (typeof _em)["DB"];
|
||||||
|
|
||||||
|
const es = [
|
||||||
|
new Entity("posts", [new TextField("name"), new TextField("slug", { required: true })]),
|
||||||
|
new Entity("comments", [new TextField("some")]),
|
||||||
|
new Entity("users", [new TextField("email")])
|
||||||
|
];
|
||||||
|
const _em2 = new EntityManager(
|
||||||
|
es,
|
||||||
|
new DummyConnection(),
|
||||||
|
[new ManyToOneRelation(es[0], es[1]), new ManyToOneRelation(es[0], es[2])],
|
||||||
|
[
|
||||||
|
new EntityIndex(es[0], [es[0].field("name")!]),
|
||||||
|
new EntityIndex(es[0], [es[0].field("slug")!], true)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
expect(_em2.toJSON()).toEqual(_em.toJSON());
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ describe("[data] Mutator (base)", async () => {
|
|||||||
new TextField("hidden", { hidden: true }),
|
new TextField("hidden", { hidden: true }),
|
||||||
new TextField("not_fillable", { fillable: false })
|
new TextField("not_fillable", { fillable: false })
|
||||||
]);
|
]);
|
||||||
const em = new EntityManager([entity], dummyConnection);
|
const em = new EntityManager<any>([entity], dummyConnection);
|
||||||
await em.schema().sync({ force: true });
|
await em.schema().sync({ force: true });
|
||||||
|
|
||||||
const payload = { label: "item 1", count: 1 };
|
const payload = { label: "item 1", count: 1 };
|
||||||
@@ -61,7 +61,7 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
|||||||
const posts = new Entity("posts", [new TextField("title")]);
|
const posts = new Entity("posts", [new TextField("title")]);
|
||||||
const users = new Entity("users", [new TextField("username")]);
|
const users = new Entity("users", [new TextField("username")]);
|
||||||
const relations = [new ManyToOneRelation(posts, users)];
|
const relations = [new ManyToOneRelation(posts, users)];
|
||||||
const em = new EntityManager([posts, users], dummyConnection, relations);
|
const em = new EntityManager<any>([posts, users], dummyConnection, relations);
|
||||||
await em.schema().sync({ force: true });
|
await em.schema().sync({ force: true });
|
||||||
|
|
||||||
test("RelationMutator", async () => {
|
test("RelationMutator", async () => {
|
||||||
@@ -192,7 +192,7 @@ describe("[data] Mutator (OneToOne)", async () => {
|
|||||||
const users = new Entity("users", [new TextField("username")]);
|
const users = new Entity("users", [new TextField("username")]);
|
||||||
const settings = new Entity("settings", [new TextField("theme")]);
|
const settings = new Entity("settings", [new TextField("theme")]);
|
||||||
const relations = [new OneToOneRelation(users, settings)];
|
const relations = [new OneToOneRelation(users, settings)];
|
||||||
const em = new EntityManager([users, settings], dummyConnection, relations);
|
const em = new EntityManager<any>([users, settings], dummyConnection, relations);
|
||||||
await em.schema().sync({ force: true });
|
await em.schema().sync({ force: true });
|
||||||
|
|
||||||
test("insertOne: missing ref", async () => {
|
test("insertOne: missing ref", async () => {
|
||||||
@@ -276,7 +276,7 @@ describe("[data] Mutator (ManyToMany)", async () => {
|
|||||||
|
|
||||||
describe("[data] Mutator (Events)", async () => {
|
describe("[data] Mutator (Events)", async () => {
|
||||||
const entity = new Entity("test", [new TextField("label")]);
|
const entity = new Entity("test", [new TextField("label")]);
|
||||||
const em = new EntityManager([entity], dummyConnection);
|
const em = new EntityManager<any>([entity], dummyConnection);
|
||||||
await em.schema().sync({ force: true });
|
await em.schema().sync({ force: true });
|
||||||
const events = new Map<string, any>();
|
const events = new Map<string, any>();
|
||||||
|
|
||||||
|
|||||||
37
app/__test__/media/mime-types.spec.ts
Normal file
37
app/__test__/media/mime-types.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import * as large from "../../src/media/storage/mime-types";
|
||||||
|
import * as tiny from "../../src/media/storage/mime-types-tiny";
|
||||||
|
|
||||||
|
describe("media/mime-types", () => {
|
||||||
|
test("tiny resolves", () => {
|
||||||
|
const tests = [[".mp4", "video/mp4", ".jpg", "image/jpeg", ".zip", "application/zip"]];
|
||||||
|
|
||||||
|
for (const [ext, mime] of tests) {
|
||||||
|
expect(tiny.guess(ext)).toBe(mime);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("all tiny resolves to large", () => {
|
||||||
|
for (const [ext, mime] of Object.entries(tiny.M)) {
|
||||||
|
expect(large.guessMimeType("." + ext)).toBe(mime);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [type, exts] of Object.entries(tiny.Q)) {
|
||||||
|
for (const ext of exts) {
|
||||||
|
const ex = `${type}/${ext}`;
|
||||||
|
try {
|
||||||
|
expect(large.guessMimeType("." + ext)).toBe(ex);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`Failed for ${ext}`, {
|
||||||
|
type,
|
||||||
|
exts,
|
||||||
|
ext,
|
||||||
|
expected: ex,
|
||||||
|
actual: large.guessMimeType("." + ext)
|
||||||
|
});
|
||||||
|
throw new Error(`Failed for ${ext}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
48
app/build.ts
48
app/build.ts
@@ -9,15 +9,43 @@ const watch = args.includes("--watch");
|
|||||||
const minify = args.includes("--minify");
|
const minify = args.includes("--minify");
|
||||||
const types = args.includes("--types");
|
const types = args.includes("--types");
|
||||||
const sourcemap = args.includes("--sourcemap");
|
const sourcemap = args.includes("--sourcemap");
|
||||||
|
const clean = args.includes("--clean");
|
||||||
|
|
||||||
|
if (clean) {
|
||||||
|
console.log("Cleaning dist");
|
||||||
await $`rm -rf dist`;
|
await $`rm -rf dist`;
|
||||||
if (types) {
|
}
|
||||||
|
|
||||||
|
let types_running = false;
|
||||||
|
function buildTypes() {
|
||||||
|
if (types_running) return;
|
||||||
|
types_running = true;
|
||||||
|
|
||||||
Bun.spawn(["bun", "build:types"], {
|
Bun.spawn(["bun", "build:types"], {
|
||||||
onExit: () => {
|
onExit: () => {
|
||||||
console.log("Types built");
|
console.log("Types built");
|
||||||
|
Bun.spawn(["bun", "tsc-alias"], {
|
||||||
|
onExit: () => {
|
||||||
|
console.log("Types aliased");
|
||||||
|
types_running = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let watcher_timeout: any;
|
||||||
|
function delayTypes() {
|
||||||
|
if (!watch) return;
|
||||||
|
if (watcher_timeout) {
|
||||||
|
clearTimeout(watcher_timeout);
|
||||||
|
}
|
||||||
|
watcher_timeout = setTimeout(buildTypes, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (types && !watch) {
|
||||||
|
buildTypes();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build static assets
|
* Build static assets
|
||||||
@@ -46,7 +74,8 @@ const result = await esbuild.build({
|
|||||||
__isDev: "0",
|
__isDev: "0",
|
||||||
"process.env.NODE_ENV": '"production"'
|
"process.env.NODE_ENV": '"production"'
|
||||||
},
|
},
|
||||||
chunkNames: "chunks/[name]-[hash]"
|
chunkNames: "chunks/[name]-[hash]",
|
||||||
|
logLevel: "error"
|
||||||
});
|
});
|
||||||
|
|
||||||
// Write manifest
|
// Write manifest
|
||||||
@@ -96,6 +125,9 @@ await tsup.build({
|
|||||||
treeshake: true,
|
treeshake: true,
|
||||||
loader: {
|
loader: {
|
||||||
".svg": "dataurl"
|
".svg": "dataurl"
|
||||||
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
delayTypes();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -117,11 +149,12 @@ await tsup.build({
|
|||||||
loader: {
|
loader: {
|
||||||
".svg": "dataurl"
|
".svg": "dataurl"
|
||||||
},
|
},
|
||||||
onSuccess: async () => {
|
|
||||||
console.log("--- ui built");
|
|
||||||
},
|
|
||||||
esbuildOptions: (options) => {
|
esbuildOptions: (options) => {
|
||||||
|
options.logLevel = "silent";
|
||||||
options.chunkNames = "chunks/[name]-[hash]";
|
options.chunkNames = "chunks/[name]-[hash]";
|
||||||
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
delayTypes();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -148,7 +181,10 @@ function baseConfig(adapter: string): tsup.Options {
|
|||||||
],
|
],
|
||||||
metafile: true,
|
metafile: true,
|
||||||
splitting: false,
|
splitting: false,
|
||||||
treeshake: true
|
treeshake: true,
|
||||||
|
onSuccess: async () => {
|
||||||
|
delayTypes();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,16 +3,16 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"bin": "./dist/cli/index.js",
|
"bin": "./dist/cli/index.js",
|
||||||
"version": "0.3.4-alpha1",
|
"version": "0.4.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:all": "bun run build && bun run build:cli",
|
"build:all": "NODE_ENV=production bun run build.ts --minify --types --clean && bun run build:cli",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"test": "ALL_TESTS=1 bun test --bail",
|
"test": "ALL_TESTS=1 bun test --bail",
|
||||||
"build": "NODE_ENV=production bun run build.ts --minify --types",
|
"build": "NODE_ENV=production bun run build.ts --minify --types",
|
||||||
"watch": "bun run build.ts --types --watch",
|
"watch": "bun run build.ts --types --watch",
|
||||||
"types": "bun tsc --noEmit",
|
"types": "bun tsc --noEmit",
|
||||||
"clean:types": "find ./dist -name '*.d.ts' -delete && rm -f ./dist/tsconfig.tsbuildinfo",
|
"clean:types": "find ./dist -name '*.d.ts' -delete && rm -f ./dist/tsconfig.tsbuildinfo",
|
||||||
"build:types": "tsc --emitDeclarationOnly",
|
"build:types": "tsc --emitDeclarationOnly && tsc-alias",
|
||||||
"build:css": "bun tailwindcss -i src/ui/main.css -o ./dist/static/styles.css",
|
"build:css": "bun tailwindcss -i src/ui/main.css -o ./dist/static/styles.css",
|
||||||
"watch:css": "bun tailwindcss --watch -i src/ui/main.css -o ./dist/styles.css",
|
"watch:css": "bun tailwindcss --watch -i src/ui/main.css -o ./dist/styles.css",
|
||||||
"updater": "bun x npm-check-updates -ui",
|
"updater": "bun x npm-check-updates -ui",
|
||||||
@@ -75,6 +75,7 @@
|
|||||||
"tailwind-merge": "^2.5.4",
|
"tailwind-merge": "^2.5.4",
|
||||||
"tailwindcss": "^3.4.14",
|
"tailwindcss": "^3.4.14",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"tsc-alias": "^1.8.10",
|
||||||
"tsup": "^8.3.5",
|
"tsup": "^8.3.5",
|
||||||
"vite": "^5.4.10",
|
"vite": "^5.4.10",
|
||||||
"vite-plugin-static-copy": "^2.0.0",
|
"vite-plugin-static-copy": "^2.0.0",
|
||||||
@@ -90,75 +91,75 @@
|
|||||||
},
|
},
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"module": "./dist/index.js",
|
"module": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/types/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/types/index.d.ts",
|
||||||
"import": "./dist/index.js",
|
"import": "./dist/index.js",
|
||||||
"require": "./dist/index.cjs"
|
"require": "./dist/index.cjs"
|
||||||
},
|
},
|
||||||
"./ui": {
|
"./ui": {
|
||||||
"types": "./dist/ui/index.d.ts",
|
"types": "./dist/types/ui/index.d.ts",
|
||||||
"import": "./dist/ui/index.js",
|
"import": "./dist/ui/index.js",
|
||||||
"require": "./dist/ui/index.cjs"
|
"require": "./dist/ui/index.cjs"
|
||||||
},
|
},
|
||||||
"./client": {
|
"./client": {
|
||||||
"types": "./dist/ui/client/index.d.ts",
|
"types": "./dist/types/ui/client/index.d.ts",
|
||||||
"import": "./dist/ui/client/index.js",
|
"import": "./dist/ui/client/index.js",
|
||||||
"require": "./dist/ui/client/index.cjs"
|
"require": "./dist/ui/client/index.cjs"
|
||||||
},
|
},
|
||||||
"./data": {
|
"./data": {
|
||||||
"types": "./dist/data/index.d.ts",
|
"types": "./dist/types/data/index.d.ts",
|
||||||
"import": "./dist/data/index.js",
|
"import": "./dist/data/index.js",
|
||||||
"require": "./dist/data/index.cjs"
|
"require": "./dist/data/index.cjs"
|
||||||
},
|
},
|
||||||
"./core": {
|
"./core": {
|
||||||
"types": "./dist/core/index.d.ts",
|
"types": "./dist/types/core/index.d.ts",
|
||||||
"import": "./dist/core/index.js",
|
"import": "./dist/core/index.js",
|
||||||
"require": "./dist/core/index.cjs"
|
"require": "./dist/core/index.cjs"
|
||||||
},
|
},
|
||||||
"./utils": {
|
"./utils": {
|
||||||
"types": "./dist/core/utils/index.d.ts",
|
"types": "./dist/types/core/utils/index.d.ts",
|
||||||
"import": "./dist/core/utils/index.js",
|
"import": "./dist/core/utils/index.js",
|
||||||
"require": "./dist/core/utils/index.cjs"
|
"require": "./dist/core/utils/index.cjs"
|
||||||
},
|
},
|
||||||
"./cli": {
|
"./cli": {
|
||||||
"types": "./dist/cli/index.d.ts",
|
"types": "./dist/types/cli/index.d.ts",
|
||||||
"import": "./dist/cli/index.js",
|
"import": "./dist/cli/index.js",
|
||||||
"require": "./dist/cli/index.cjs"
|
"require": "./dist/cli/index.cjs"
|
||||||
},
|
},
|
||||||
"./adapter/cloudflare": {
|
"./adapter/cloudflare": {
|
||||||
"types": "./dist/adapter/cloudflare/index.d.ts",
|
"types": "./dist/types/adapter/cloudflare/index.d.ts",
|
||||||
"import": "./dist/adapter/cloudflare/index.js",
|
"import": "./dist/adapter/cloudflare/index.js",
|
||||||
"require": "./dist/adapter/cloudflare/index.cjs"
|
"require": "./dist/adapter/cloudflare/index.cjs"
|
||||||
},
|
},
|
||||||
"./adapter/vite": {
|
"./adapter/vite": {
|
||||||
"types": "./dist/adapter/vite/index.d.ts",
|
"types": "./dist/types/adapter/vite/index.d.ts",
|
||||||
"import": "./dist/adapter/vite/index.js",
|
"import": "./dist/adapter/vite/index.js",
|
||||||
"require": "./dist/adapter/vite/index.cjs"
|
"require": "./dist/adapter/vite/index.cjs"
|
||||||
},
|
},
|
||||||
"./adapter/nextjs": {
|
"./adapter/nextjs": {
|
||||||
"types": "./dist/adapter/nextjs/index.d.ts",
|
"types": "./dist/types/adapter/nextjs/index.d.ts",
|
||||||
"import": "./dist/adapter/nextjs/index.js",
|
"import": "./dist/adapter/nextjs/index.js",
|
||||||
"require": "./dist/adapter/nextjs/index.cjs"
|
"require": "./dist/adapter/nextjs/index.cjs"
|
||||||
},
|
},
|
||||||
"./adapter/remix": {
|
"./adapter/remix": {
|
||||||
"types": "./dist/adapter/remix/index.d.ts",
|
"types": "./dist/types/adapter/remix/index.d.ts",
|
||||||
"import": "./dist/adapter/remix/index.js",
|
"import": "./dist/adapter/remix/index.js",
|
||||||
"require": "./dist/adapter/remix/index.cjs"
|
"require": "./dist/adapter/remix/index.cjs"
|
||||||
},
|
},
|
||||||
"./adapter/bun": {
|
"./adapter/bun": {
|
||||||
"types": "./dist/adapter/bun/index.d.ts",
|
"types": "./dist/types/adapter/bun/index.d.ts",
|
||||||
"import": "./dist/adapter/bun/index.js",
|
"import": "./dist/adapter/bun/index.js",
|
||||||
"require": "./dist/adapter/bun/index.cjs"
|
"require": "./dist/adapter/bun/index.cjs"
|
||||||
},
|
},
|
||||||
"./adapter/node": {
|
"./adapter/node": {
|
||||||
"types": "./dist/adapter/node/index.d.ts",
|
"types": "./dist/types/adapter/node/index.d.ts",
|
||||||
"import": "./dist/adapter/node/index.js",
|
"import": "./dist/adapter/node/index.js",
|
||||||
"require": "./dist/adapter/node/index.cjs"
|
"require": "./dist/adapter/node/index.cjs"
|
||||||
},
|
},
|
||||||
"./adapter/astro": {
|
"./adapter/astro": {
|
||||||
"types": "./dist/adapter/astro/index.d.ts",
|
"types": "./dist/types/adapter/astro/index.d.ts",
|
||||||
"import": "./dist/adapter/astro/index.js",
|
"import": "./dist/adapter/astro/index.js",
|
||||||
"require": "./dist/adapter/astro/index.cjs"
|
"require": "./dist/adapter/astro/index.cjs"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -128,6 +128,14 @@ export class Api {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getVerifiedAuthState(force?: boolean): Promise<AuthState> {
|
||||||
|
if (force === true || !this.verified) {
|
||||||
|
await this.verifyAuth();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getAuthState();
|
||||||
|
}
|
||||||
|
|
||||||
async verifyAuth() {
|
async verifyAuth() {
|
||||||
try {
|
try {
|
||||||
const res = await this.auth.me();
|
const res = await this.auth.me();
|
||||||
|
|||||||
@@ -10,15 +10,19 @@ import * as SystemPermissions from "modules/permissions";
|
|||||||
import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
|
import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
|
||||||
import { SystemController } from "modules/server/SystemController";
|
import { SystemController } from "modules/server/SystemController";
|
||||||
|
|
||||||
export type AppPlugin<DB> = (app: App<DB>) => void;
|
export type AppPlugin = (app: App) => Promise<void> | void;
|
||||||
|
|
||||||
export class AppConfigUpdatedEvent extends Event<{ app: App }> {
|
abstract class AppEvent<A = {}> extends Event<{ app: App } & A> {}
|
||||||
|
export class AppConfigUpdatedEvent extends AppEvent {
|
||||||
static override slug = "app-config-updated";
|
static override slug = "app-config-updated";
|
||||||
}
|
}
|
||||||
export class AppBuiltEvent extends Event<{ app: App }> {
|
export class AppBuiltEvent extends AppEvent {
|
||||||
static override slug = "app-built";
|
static override slug = "app-built";
|
||||||
}
|
}
|
||||||
export const AppEvents = { AppConfigUpdatedEvent, AppBuiltEvent } as const;
|
export class AppFirstBoot extends AppEvent {
|
||||||
|
static override slug = "app-first-boot";
|
||||||
|
}
|
||||||
|
export const AppEvents = { AppConfigUpdatedEvent, AppBuiltEvent, AppFirstBoot } as const;
|
||||||
|
|
||||||
export type CreateAppConfig = {
|
export type CreateAppConfig = {
|
||||||
connection?:
|
connection?:
|
||||||
@@ -28,29 +32,42 @@ export type CreateAppConfig = {
|
|||||||
config: LibSqlCredentials;
|
config: LibSqlCredentials;
|
||||||
};
|
};
|
||||||
initialConfig?: InitialModuleConfigs;
|
initialConfig?: InitialModuleConfigs;
|
||||||
plugins?: AppPlugin<any>[];
|
plugins?: AppPlugin[];
|
||||||
options?: Omit<ModuleManagerOptions, "initial" | "onUpdated">;
|
options?: Omit<ModuleManagerOptions, "initial" | "onUpdated">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AppConfig = InitialModuleConfigs;
|
export type AppConfig = InitialModuleConfigs;
|
||||||
|
|
||||||
export class App<DB = any> {
|
export class App {
|
||||||
modules: ModuleManager;
|
modules: ModuleManager;
|
||||||
static readonly Events = AppEvents;
|
static readonly Events = AppEvents;
|
||||||
|
adminController?: AdminController;
|
||||||
|
private trigger_first_boot = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private connection: Connection,
|
private connection: Connection,
|
||||||
_initialConfig?: InitialModuleConfigs,
|
_initialConfig?: InitialModuleConfigs,
|
||||||
private plugins: AppPlugin<DB>[] = [],
|
private plugins: AppPlugin[] = [],
|
||||||
moduleManagerOptions?: ModuleManagerOptions
|
moduleManagerOptions?: ModuleManagerOptions
|
||||||
) {
|
) {
|
||||||
this.modules = new ModuleManager(connection, {
|
this.modules = new ModuleManager(connection, {
|
||||||
...moduleManagerOptions,
|
...moduleManagerOptions,
|
||||||
initial: _initialConfig,
|
initial: _initialConfig,
|
||||||
onUpdated: async (key, config) => {
|
onUpdated: async (key, config) => {
|
||||||
//console.log("[APP] config updated", key, config);
|
// if the EventManager was disabled, we assume we shouldn't
|
||||||
|
// respond to events, such as "onUpdated".
|
||||||
|
if (!this.emgr.enabled) {
|
||||||
|
console.warn("[APP] config updated, but event manager is disabled, skip.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[APP] config updated", key);
|
||||||
await this.build({ sync: true, save: true });
|
await this.build({ sync: true, save: true });
|
||||||
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this }));
|
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this }));
|
||||||
|
},
|
||||||
|
onFirstBoot: async () => {
|
||||||
|
console.log("[APP] first boot");
|
||||||
|
this.trigger_first_boot = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.modules.ctx().emgr.registerEvents(AppEvents);
|
this.modules.ctx().emgr.registerEvents(AppEvents);
|
||||||
@@ -76,7 +93,7 @@ export class App<DB = any> {
|
|||||||
|
|
||||||
// load plugins
|
// load plugins
|
||||||
if (this.plugins.length > 0) {
|
if (this.plugins.length > 0) {
|
||||||
this.plugins.forEach((plugin) => plugin(this));
|
await Promise.all(this.plugins.map((plugin) => plugin(this)));
|
||||||
}
|
}
|
||||||
|
|
||||||
//console.log("emitting built", options);
|
//console.log("emitting built", options);
|
||||||
@@ -88,14 +105,24 @@ export class App<DB = any> {
|
|||||||
if (options?.save) {
|
if (options?.save) {
|
||||||
await this.modules.save();
|
await this.modules.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// first boot is set from ModuleManager when there wasn't a config table
|
||||||
|
if (this.trigger_first_boot) {
|
||||||
|
this.trigger_first_boot = false;
|
||||||
|
await this.emgr.emit(new AppFirstBoot({ app: this }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateConfig<Module extends keyof Modules>(module: Module) {
|
mutateConfig<Module extends keyof Modules>(module: Module) {
|
||||||
return this.modules.get(module).schema();
|
return this.modules.get(module).schema();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get server() {
|
||||||
|
return this.modules.server;
|
||||||
|
}
|
||||||
|
|
||||||
get fetch(): any {
|
get fetch(): any {
|
||||||
return this.modules.server.fetch;
|
return this.server.fetch;
|
||||||
}
|
}
|
||||||
|
|
||||||
get module() {
|
get module() {
|
||||||
@@ -119,7 +146,8 @@ export class App<DB = any> {
|
|||||||
|
|
||||||
registerAdminController(config?: AdminControllerOptions) {
|
registerAdminController(config?: AdminControllerOptions) {
|
||||||
// register admin
|
// register admin
|
||||||
this.modules.server.route("/", new AdminController(this, config).getController());
|
this.adminController = new AdminController(this, config);
|
||||||
|
this.modules.server.route("/", this.adminController.getController());
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { Api, type ApiOptions, App, type CreateAppConfig } from "bknd";
|
import { type FrameworkBkndConfig, createFrameworkApp } from "adapter";
|
||||||
|
import { Api, type ApiOptions, type App } from "bknd";
|
||||||
|
|
||||||
|
export type AstroBkndConfig = FrameworkBkndConfig;
|
||||||
|
|
||||||
type TAstro = {
|
type TAstro = {
|
||||||
request: Request;
|
request: Request;
|
||||||
@@ -18,12 +21,10 @@ export function getApi(Astro: TAstro, options: Options = { mode: "static" }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let app: App;
|
let app: App;
|
||||||
export function serve(config: CreateAppConfig) {
|
export function serve(config: AstroBkndConfig = {}) {
|
||||||
return async (args: TAstro) => {
|
return async (args: TAstro) => {
|
||||||
if (!app) {
|
if (!app) {
|
||||||
app = App.create(config);
|
app = await createFrameworkApp(config);
|
||||||
|
|
||||||
await app.build();
|
|
||||||
}
|
}
|
||||||
return app.fetch(args.request);
|
return app.fetch(args.request);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,56 +1,60 @@
|
|||||||
/// <reference types="bun-types" />
|
/// <reference types="bun-types" />
|
||||||
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { App, type CreateAppConfig } from "bknd";
|
import type { App } from "bknd";
|
||||||
import type { Serve, ServeOptions } from "bun";
|
import type { ServeOptions } from "bun";
|
||||||
|
import { config } from "core";
|
||||||
import { serveStatic } from "hono/bun";
|
import { serveStatic } from "hono/bun";
|
||||||
|
import { type RuntimeBkndConfig, createRuntimeApp } from "../index";
|
||||||
|
|
||||||
let app: App;
|
let app: App;
|
||||||
export async function createApp(_config: Partial<CreateAppConfig> = {}, distPath?: string) {
|
|
||||||
|
export type BunBkndConfig = RuntimeBkndConfig & Omit<ServeOptions, "fetch">;
|
||||||
|
|
||||||
|
export async function createApp({
|
||||||
|
distPath,
|
||||||
|
onBuilt,
|
||||||
|
buildConfig,
|
||||||
|
beforeBuild,
|
||||||
|
...config
|
||||||
|
}: RuntimeBkndConfig = {}) {
|
||||||
const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static");
|
const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static");
|
||||||
|
|
||||||
if (!app) {
|
if (!app) {
|
||||||
app = App.create(_config);
|
app = await createRuntimeApp({
|
||||||
|
...config,
|
||||||
app.emgr.on(
|
registerLocalMedia: true,
|
||||||
"app-built",
|
serveStatic: serveStatic({ root })
|
||||||
async () => {
|
});
|
||||||
app.modules.server.get(
|
|
||||||
"/*",
|
|
||||||
serveStatic({
|
|
||||||
root
|
|
||||||
})
|
|
||||||
);
|
|
||||||
app.registerAdminController();
|
|
||||||
},
|
|
||||||
"sync"
|
|
||||||
);
|
|
||||||
|
|
||||||
await app.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BunAdapterOptions = Omit<ServeOptions, "fetch"> &
|
|
||||||
CreateAppConfig & {
|
|
||||||
distPath?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function serve({
|
export function serve({
|
||||||
distPath,
|
distPath,
|
||||||
connection,
|
connection,
|
||||||
initialConfig,
|
initialConfig,
|
||||||
plugins,
|
plugins,
|
||||||
options,
|
options,
|
||||||
port = 1337,
|
port = config.server.default_port,
|
||||||
|
onBuilt,
|
||||||
|
buildConfig,
|
||||||
...serveOptions
|
...serveOptions
|
||||||
}: BunAdapterOptions = {}) {
|
}: BunBkndConfig = {}) {
|
||||||
Bun.serve({
|
Bun.serve({
|
||||||
...serveOptions,
|
...serveOptions,
|
||||||
port,
|
port,
|
||||||
fetch: async (request: Request) => {
|
fetch: async (request: Request) => {
|
||||||
const app = await createApp({ connection, initialConfig, plugins, options }, distPath);
|
const app = await createApp({
|
||||||
|
connection,
|
||||||
|
initialConfig,
|
||||||
|
plugins,
|
||||||
|
options,
|
||||||
|
onBuilt,
|
||||||
|
buildConfig,
|
||||||
|
distPath
|
||||||
|
});
|
||||||
return app.fetch(request);
|
return app.fetch(request);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,21 +1,37 @@
|
|||||||
import { DurableObject } from "cloudflare:workers";
|
import type { CreateAppConfig } from "bknd";
|
||||||
import { App, type CreateAppConfig } from "bknd";
|
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { serveStatic } from "hono/cloudflare-workers";
|
import { serveStatic } from "hono/cloudflare-workers";
|
||||||
import type { BkndConfig, CfBkndModeCache } from "../index";
|
import type { FrameworkBkndConfig } from "../index";
|
||||||
|
import { getCached } from "./modes/cached";
|
||||||
|
import { getDurable } from "./modes/durable";
|
||||||
|
import { getFresh, getWarm } from "./modes/fresh";
|
||||||
|
|
||||||
type Context = {
|
export type CloudflareBkndConfig<Env = any> = Omit<FrameworkBkndConfig, "app"> & {
|
||||||
request: Request;
|
app: CreateAppConfig | ((env: Env) => CreateAppConfig);
|
||||||
env: any;
|
mode?: "warm" | "fresh" | "cache" | "durable";
|
||||||
ctx: ExecutionContext;
|
bindings?: (env: Env) => {
|
||||||
manifest: any;
|
kv?: KVNamespace;
|
||||||
|
dobj?: DurableObjectNamespace;
|
||||||
|
};
|
||||||
|
key?: string;
|
||||||
|
keepAliveSeconds?: number;
|
||||||
|
forceHttps?: boolean;
|
||||||
|
manifest?: string;
|
||||||
|
setAdminHtml?: boolean;
|
||||||
html?: string;
|
html?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function serve(_config: BkndConfig, manifest?: string, html?: string) {
|
export type Context = {
|
||||||
|
request: Request;
|
||||||
|
env: any;
|
||||||
|
ctx: ExecutionContext;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function serve(config: CloudflareBkndConfig) {
|
||||||
return {
|
return {
|
||||||
async fetch(request: Request, env: any, ctx: ExecutionContext) {
|
async fetch(request: Request, env: any, ctx: ExecutionContext) {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
|
const manifest = config.manifest;
|
||||||
|
|
||||||
if (manifest) {
|
if (manifest) {
|
||||||
const pathname = url.pathname.slice(1);
|
const pathname = url.pathname.slice(1);
|
||||||
@@ -26,13 +42,10 @@ export function serve(_config: BkndConfig, manifest?: string, html?: string) {
|
|||||||
hono.all("*", async (c, next) => {
|
hono.all("*", async (c, next) => {
|
||||||
const res = await serveStatic({
|
const res = await serveStatic({
|
||||||
path: `./${pathname}`,
|
path: `./${pathname}`,
|
||||||
manifest,
|
manifest
|
||||||
onNotFound: (path) => console.log("not found", path)
|
|
||||||
})(c as any, next);
|
})(c as any, next);
|
||||||
if (res instanceof Response) {
|
if (res instanceof Response) {
|
||||||
const ttl = pathname.startsWith("assets/")
|
const ttl = 60 * 60 * 24 * 365;
|
||||||
? 60 * 60 * 24 * 365 // 1 year
|
|
||||||
: 60 * 5; // 5 minutes
|
|
||||||
res.headers.set("Cache-Control", `public, max-age=${ttl}`);
|
res.headers.set("Cache-Control", `public, max-age=${ttl}`);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
@@ -44,218 +57,23 @@ export function serve(_config: BkndConfig, manifest?: string, html?: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = {
|
config.setAdminHtml = config.setAdminHtml && !!config.manifest;
|
||||||
..._config,
|
|
||||||
setAdminHtml: _config.setAdminHtml ?? !!manifest
|
|
||||||
};
|
|
||||||
const context = { request, env, ctx, manifest, html };
|
|
||||||
const mode = config.cloudflare?.mode?.(env);
|
|
||||||
|
|
||||||
if (!mode) {
|
const context = { request, env, ctx } as Context;
|
||||||
console.log("serving fresh...");
|
const mode = config.mode ?? "warm";
|
||||||
const app = await getFresh(config, context);
|
|
||||||
return app.fetch(request, env);
|
|
||||||
} else if ("cache" in mode) {
|
|
||||||
console.log("serving cached...");
|
|
||||||
const app = await getCached(config as any, context);
|
|
||||||
return app.fetch(request, env);
|
|
||||||
} else if ("durableObject" in mode) {
|
|
||||||
console.log("serving durable...");
|
|
||||||
|
|
||||||
if (config.onBuilt) {
|
switch (mode) {
|
||||||
console.log("onBuilt() is not supported with DurableObject mode");
|
case "fresh":
|
||||||
}
|
return await getFresh(config, context);
|
||||||
|
case "warm":
|
||||||
const start = performance.now();
|
return await getWarm(config, context);
|
||||||
|
case "cache":
|
||||||
const durable = mode.durableObject;
|
return await getCached(config, context);
|
||||||
const id = durable.idFromName(mode.key);
|
case "durable":
|
||||||
const stub = durable.get(id) as unknown as DurableBkndApp;
|
return await getDurable(config, context);
|
||||||
|
default:
|
||||||
const create_config = typeof config.app === "function" ? config.app(env) : config.app;
|
throw new Error(`Unknown mode ${mode}`);
|
||||||
|
|
||||||
const res = await stub.fire(request, {
|
|
||||||
config: create_config,
|
|
||||||
html,
|
|
||||||
keepAliveSeconds: mode.keepAliveSeconds,
|
|
||||||
setAdminHtml: config.setAdminHtml
|
|
||||||
});
|
|
||||||
|
|
||||||
const headers = new Headers(res.headers);
|
|
||||||
headers.set("X-TTDO", String(performance.now() - start));
|
|
||||||
|
|
||||||
return new Response(res.body, {
|
|
||||||
status: res.status,
|
|
||||||
statusText: res.statusText,
|
|
||||||
headers
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getFresh(config: BkndConfig, { env, html }: Context) {
|
|
||||||
const create_config = typeof config.app === "function" ? config.app(env) : config.app;
|
|
||||||
const app = App.create(create_config);
|
|
||||||
|
|
||||||
if (config.onBuilt) {
|
|
||||||
app.emgr.onEvent(
|
|
||||||
App.Events.AppBuiltEvent,
|
|
||||||
async ({ params: { app } }) => {
|
|
||||||
config.onBuilt!(app);
|
|
||||||
},
|
|
||||||
"sync"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await app.build();
|
|
||||||
|
|
||||||
if (config.setAdminHtml) {
|
|
||||||
app.registerAdminController({ html });
|
|
||||||
}
|
|
||||||
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getCached(
|
|
||||||
config: BkndConfig & { cloudflare: { mode: CfBkndModeCache } },
|
|
||||||
{ env, html, ctx }: Context
|
|
||||||
) {
|
|
||||||
const { cache, key } = config.cloudflare.mode(env) as ReturnType<CfBkndModeCache>;
|
|
||||||
const create_config = typeof config.app === "function" ? config.app(env) : config.app;
|
|
||||||
|
|
||||||
const cachedConfig = await cache.get(key);
|
|
||||||
const initialConfig = cachedConfig ? JSON.parse(cachedConfig) : undefined;
|
|
||||||
|
|
||||||
const app = App.create({ ...create_config, initialConfig });
|
|
||||||
|
|
||||||
async function saveConfig(__config: any) {
|
|
||||||
ctx.waitUntil(cache.put(key, JSON.stringify(__config)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.onBuilt) {
|
|
||||||
app.emgr.onEvent(
|
|
||||||
App.Events.AppBuiltEvent,
|
|
||||||
async ({ params: { app } }) => {
|
|
||||||
app.module.server.client.get("/__bknd/cache", async (c) => {
|
|
||||||
await cache.delete(key);
|
|
||||||
return c.json({ message: "Cache cleared" });
|
|
||||||
});
|
|
||||||
app.registerAdminController({ html });
|
|
||||||
|
|
||||||
config.onBuilt!(app);
|
|
||||||
},
|
|
||||||
"sync"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
app.emgr.onEvent(
|
|
||||||
App.Events.AppConfigUpdatedEvent,
|
|
||||||
async ({ params: { app } }) => {
|
|
||||||
saveConfig(app.toJSON(true));
|
|
||||||
},
|
|
||||||
"sync"
|
|
||||||
);
|
|
||||||
|
|
||||||
await app.build();
|
|
||||||
|
|
||||||
if (config.setAdminHtml) {
|
|
||||||
app.registerAdminController({ html });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cachedConfig) {
|
|
||||||
saveConfig(app.toJSON(true));
|
|
||||||
}
|
|
||||||
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DurableBkndApp extends DurableObject {
|
|
||||||
protected id = Math.random().toString(36).slice(2);
|
|
||||||
protected app?: App;
|
|
||||||
protected interval?: any;
|
|
||||||
|
|
||||||
async fire(
|
|
||||||
request: Request,
|
|
||||||
options: {
|
|
||||||
config: CreateAppConfig;
|
|
||||||
html?: string;
|
|
||||||
keepAliveSeconds?: number;
|
|
||||||
setAdminHtml?: boolean;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
let buildtime = 0;
|
|
||||||
if (!this.app) {
|
|
||||||
const start = performance.now();
|
|
||||||
const config = options.config;
|
|
||||||
|
|
||||||
// change protocol to websocket if libsql
|
|
||||||
if (
|
|
||||||
config?.connection &&
|
|
||||||
"type" in config.connection &&
|
|
||||||
config.connection.type === "libsql"
|
|
||||||
) {
|
|
||||||
config.connection.config.protocol = "wss";
|
|
||||||
}
|
|
||||||
|
|
||||||
this.app = App.create(config);
|
|
||||||
this.app.emgr.onEvent(
|
|
||||||
App.Events.AppBuiltEvent,
|
|
||||||
async ({ params: { app } }) => {
|
|
||||||
app.modules.server.get("/__do", async (c) => {
|
|
||||||
// @ts-ignore
|
|
||||||
const context: any = c.req.raw.cf ? c.req.raw.cf : c.env.cf;
|
|
||||||
return c.json({
|
|
||||||
id: this.id,
|
|
||||||
keepAlive: options?.keepAliveSeconds,
|
|
||||||
colo: context.colo
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
"sync"
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.app.build();
|
|
||||||
|
|
||||||
buildtime = performance.now() - start;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options?.keepAliveSeconds) {
|
|
||||||
this.keepAlive(options.keepAliveSeconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("id", this.id);
|
|
||||||
const res = await this.app!.fetch(request);
|
|
||||||
const headers = new Headers(res.headers);
|
|
||||||
headers.set("X-BuildTime", buildtime.toString());
|
|
||||||
headers.set("X-DO-ID", this.id);
|
|
||||||
|
|
||||||
return new Response(res.body, {
|
|
||||||
status: res.status,
|
|
||||||
statusText: res.statusText,
|
|
||||||
headers
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected keepAlive(seconds: number) {
|
|
||||||
console.log("keep alive for", seconds);
|
|
||||||
if (this.interval) {
|
|
||||||
console.log("clearing, there is a new");
|
|
||||||
clearInterval(this.interval);
|
|
||||||
}
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
this.interval = setInterval(() => {
|
|
||||||
i += 1;
|
|
||||||
//console.log("keep-alive", i);
|
|
||||||
if (i === seconds) {
|
|
||||||
console.log("cleared");
|
|
||||||
clearInterval(this.interval);
|
|
||||||
|
|
||||||
// ping every 30 seconds
|
|
||||||
} else if (i % 30 === 0) {
|
|
||||||
console.log("ping");
|
|
||||||
this.app?.modules.ctx().connection.ping();
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1 +1,4 @@
|
|||||||
export * from "./cloudflare-workers.adapter";
|
export * from "./cloudflare-workers.adapter";
|
||||||
|
export { makeApp, getFresh, getWarm } from "./modes/fresh";
|
||||||
|
export { getCached } from "./modes/cached";
|
||||||
|
export { DurableBkndApp, getDurable } from "./modes/durable";
|
||||||
|
|||||||
48
app/src/adapter/cloudflare/modes/cached.ts
Normal file
48
app/src/adapter/cloudflare/modes/cached.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { createRuntimeApp } from "adapter";
|
||||||
|
import { App } from "bknd";
|
||||||
|
import type { CloudflareBkndConfig, Context } from "../index";
|
||||||
|
|
||||||
|
export async function getCached(config: CloudflareBkndConfig, { env, ctx }: Context) {
|
||||||
|
const { kv } = config.bindings?.(env)!;
|
||||||
|
if (!kv) throw new Error("kv namespace is not defined in cloudflare.bindings");
|
||||||
|
const key = config.key ?? "app";
|
||||||
|
|
||||||
|
const cachedConfig = await kv.get(key);
|
||||||
|
const initialConfig = cachedConfig ? JSON.parse(cachedConfig) : undefined;
|
||||||
|
|
||||||
|
async function saveConfig(__config: any) {
|
||||||
|
ctx.waitUntil(kv!.put(key, JSON.stringify(__config)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = await createRuntimeApp(
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
initialConfig,
|
||||||
|
onBuilt: async (app) => {
|
||||||
|
app.module.server.client.get("/__bknd/cache", async (c) => {
|
||||||
|
await kv.delete(key);
|
||||||
|
return c.json({ message: "Cache cleared" });
|
||||||
|
});
|
||||||
|
await config.onBuilt?.(app);
|
||||||
|
},
|
||||||
|
beforeBuild: async (app) => {
|
||||||
|
app.emgr.onEvent(
|
||||||
|
App.Events.AppConfigUpdatedEvent,
|
||||||
|
async ({ params: { app } }) => {
|
||||||
|
saveConfig(app.toJSON(true));
|
||||||
|
},
|
||||||
|
"sync"
|
||||||
|
);
|
||||||
|
await config.beforeBuild?.(app);
|
||||||
|
},
|
||||||
|
adminOptions: { html: config.html }
|
||||||
|
},
|
||||||
|
env
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!cachedConfig) {
|
||||||
|
saveConfig(app.toJSON(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
133
app/src/adapter/cloudflare/modes/durable.ts
Normal file
133
app/src/adapter/cloudflare/modes/durable.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import { DurableObject } from "cloudflare:workers";
|
||||||
|
import { createRuntimeApp } from "adapter";
|
||||||
|
import type { CloudflareBkndConfig, Context } from "adapter/cloudflare";
|
||||||
|
import type { App, CreateAppConfig } from "bknd";
|
||||||
|
|
||||||
|
export async function getDurable(config: CloudflareBkndConfig, ctx: Context) {
|
||||||
|
const { dobj } = config.bindings?.(ctx.env)!;
|
||||||
|
if (!dobj) throw new Error("durable object is not defined in cloudflare.bindings");
|
||||||
|
const key = config.key ?? "app";
|
||||||
|
|
||||||
|
if ([config.onBuilt, config.beforeBuild].some((x) => x)) {
|
||||||
|
console.log("onBuilt and beforeBuild are not supported with DurableObject mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = performance.now();
|
||||||
|
|
||||||
|
const id = dobj.idFromName(key);
|
||||||
|
const stub = dobj.get(id) as unknown as DurableBkndApp;
|
||||||
|
|
||||||
|
const create_config = typeof config.app === "function" ? config.app(ctx.env) : config.app;
|
||||||
|
|
||||||
|
const res = await stub.fire(ctx.request, {
|
||||||
|
config: create_config,
|
||||||
|
html: config.html,
|
||||||
|
keepAliveSeconds: config.keepAliveSeconds,
|
||||||
|
setAdminHtml: config.setAdminHtml
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers = new Headers(res.headers);
|
||||||
|
headers.set("X-TTDO", String(performance.now() - start));
|
||||||
|
|
||||||
|
return new Response(res.body, {
|
||||||
|
status: res.status,
|
||||||
|
statusText: res.statusText,
|
||||||
|
headers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DurableBkndApp extends DurableObject {
|
||||||
|
protected id = Math.random().toString(36).slice(2);
|
||||||
|
protected app?: App;
|
||||||
|
protected interval?: any;
|
||||||
|
|
||||||
|
async fire(
|
||||||
|
request: Request,
|
||||||
|
options: {
|
||||||
|
config: CreateAppConfig;
|
||||||
|
html?: string;
|
||||||
|
keepAliveSeconds?: number;
|
||||||
|
setAdminHtml?: boolean;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
let buildtime = 0;
|
||||||
|
if (!this.app) {
|
||||||
|
const start = performance.now();
|
||||||
|
const config = options.config;
|
||||||
|
|
||||||
|
// change protocol to websocket if libsql
|
||||||
|
if (
|
||||||
|
config?.connection &&
|
||||||
|
"type" in config.connection &&
|
||||||
|
config.connection.type === "libsql"
|
||||||
|
) {
|
||||||
|
config.connection.config.protocol = "wss";
|
||||||
|
}
|
||||||
|
|
||||||
|
this.app = await createRuntimeApp({
|
||||||
|
...config,
|
||||||
|
onBuilt: async (app) => {
|
||||||
|
app.modules.server.get("/__do", async (c) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const context: any = c.req.raw.cf ? c.req.raw.cf : c.env.cf;
|
||||||
|
return c.json({
|
||||||
|
id: this.id,
|
||||||
|
keepAliveSeconds: options?.keepAliveSeconds ?? 0,
|
||||||
|
colo: context.colo
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.onBuilt(app);
|
||||||
|
},
|
||||||
|
adminOptions: { html: options.html },
|
||||||
|
beforeBuild: async (app) => {
|
||||||
|
await this.beforeBuild(app);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
buildtime = performance.now() - start;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.keepAliveSeconds) {
|
||||||
|
this.keepAlive(options.keepAliveSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("id", this.id);
|
||||||
|
const res = await this.app!.fetch(request);
|
||||||
|
const headers = new Headers(res.headers);
|
||||||
|
headers.set("X-BuildTime", buildtime.toString());
|
||||||
|
headers.set("X-DO-ID", this.id);
|
||||||
|
|
||||||
|
return new Response(res.body, {
|
||||||
|
status: res.status,
|
||||||
|
statusText: res.statusText,
|
||||||
|
headers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onBuilt(app: App) {}
|
||||||
|
async beforeBuild(app: App) {}
|
||||||
|
|
||||||
|
protected keepAlive(seconds: number) {
|
||||||
|
console.log("keep alive for", seconds);
|
||||||
|
if (this.interval) {
|
||||||
|
console.log("clearing, there is a new");
|
||||||
|
clearInterval(this.interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
this.interval = setInterval(() => {
|
||||||
|
i += 1;
|
||||||
|
//console.log("keep-alive", i);
|
||||||
|
if (i === seconds) {
|
||||||
|
console.log("cleared");
|
||||||
|
clearInterval(this.interval);
|
||||||
|
|
||||||
|
// ping every 30 seconds
|
||||||
|
} else if (i % 30 === 0) {
|
||||||
|
console.log("ping");
|
||||||
|
this.app?.modules.ctx().connection.ping();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
app/src/adapter/cloudflare/modes/fresh.ts
Normal file
27
app/src/adapter/cloudflare/modes/fresh.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { createRuntimeApp } from "adapter";
|
||||||
|
import type { App } from "bknd";
|
||||||
|
import type { CloudflareBkndConfig, Context } from "../index";
|
||||||
|
|
||||||
|
export async function makeApp(config: CloudflareBkndConfig, { env }: Context) {
|
||||||
|
return await createRuntimeApp(
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
adminOptions: config.html ? { html: config.html } : undefined
|
||||||
|
},
|
||||||
|
env
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFresh(config: CloudflareBkndConfig, ctx: Context) {
|
||||||
|
const app = await makeApp(config, ctx);
|
||||||
|
return app.fetch(ctx.request);
|
||||||
|
}
|
||||||
|
|
||||||
|
let warm_app: App;
|
||||||
|
export async function getWarm(config: CloudflareBkndConfig, ctx: Context) {
|
||||||
|
if (!warm_app) {
|
||||||
|
warm_app = await makeApp(config, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
return warm_app.fetch(ctx.request);
|
||||||
|
}
|
||||||
@@ -1,40 +1,20 @@
|
|||||||
import type { IncomingMessage } from "node:http";
|
import type { IncomingMessage } from "node:http";
|
||||||
import type { App, CreateAppConfig } from "bknd";
|
import { App, type CreateAppConfig, registries } from "bknd";
|
||||||
|
import type { MiddlewareHandler } from "hono";
|
||||||
|
import { StorageLocalAdapter } from "media/storage/adapters/StorageLocalAdapter";
|
||||||
|
import type { AdminControllerOptions } from "modules/server/AdminController";
|
||||||
|
|
||||||
export type CfBkndModeCache<Env = any> = (env: Env) => {
|
export type BkndConfig<Env = any> = CreateAppConfig & {
|
||||||
cache: KVNamespace;
|
app?: CreateAppConfig | ((env: Env) => CreateAppConfig);
|
||||||
key: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CfBkndModeDurableObject<Env = any> = (env: Env) => {
|
|
||||||
durableObject: DurableObjectNamespace;
|
|
||||||
key: string;
|
|
||||||
keepAliveSeconds?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CloudflareBkndConfig<Env = any> = {
|
|
||||||
mode?: CfBkndModeCache | CfBkndModeDurableObject;
|
|
||||||
forceHttps?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
// @todo: move to App
|
|
||||||
export type BkndConfig<Env = any> = {
|
|
||||||
app: CreateAppConfig | ((env: Env) => CreateAppConfig);
|
|
||||||
setAdminHtml?: boolean;
|
|
||||||
server?: {
|
|
||||||
port?: number;
|
|
||||||
platform?: "node" | "bun";
|
|
||||||
};
|
|
||||||
cloudflare?: CloudflareBkndConfig<Env>;
|
|
||||||
onBuilt?: (app: App) => Promise<void>;
|
onBuilt?: (app: App) => Promise<void>;
|
||||||
|
beforeBuild?: (app: App) => Promise<void>;
|
||||||
|
buildConfig?: Parameters<App["build"]>[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BkndConfigJson = {
|
export type FrameworkBkndConfig<Env = any> = BkndConfig<Env>;
|
||||||
app: CreateAppConfig;
|
|
||||||
setAdminHtml?: boolean;
|
export type RuntimeBkndConfig<Env = any> = BkndConfig<Env> & {
|
||||||
server?: {
|
distPath?: string;
|
||||||
port?: number;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function nodeRequestToRequest(req: IncomingMessage): Request {
|
export function nodeRequestToRequest(req: IncomingMessage): Request {
|
||||||
@@ -60,3 +40,90 @@ export function nodeRequestToRequest(req: IncomingMessage): Request {
|
|||||||
headers
|
headers
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function registerLocalMediaAdapter() {
|
||||||
|
registries.media.register("local", StorageLocalAdapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeConfig<Env = any>(config: BkndConfig<Env>, env?: Env): CreateAppConfig {
|
||||||
|
let additionalConfig: CreateAppConfig = {};
|
||||||
|
if ("app" in config && config.app) {
|
||||||
|
if (typeof config.app === "function") {
|
||||||
|
if (!env) {
|
||||||
|
throw new Error("env is required when config.app is a function");
|
||||||
|
}
|
||||||
|
additionalConfig = config.app(env);
|
||||||
|
} else {
|
||||||
|
additionalConfig = config.app;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...config, ...additionalConfig };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createFrameworkApp<Env = any>(
|
||||||
|
config: FrameworkBkndConfig,
|
||||||
|
env?: Env
|
||||||
|
): Promise<App> {
|
||||||
|
const app = App.create(makeConfig(config, env));
|
||||||
|
|
||||||
|
if (config.onBuilt) {
|
||||||
|
app.emgr.onEvent(
|
||||||
|
App.Events.AppBuiltEvent,
|
||||||
|
async () => {
|
||||||
|
await config.onBuilt?.(app);
|
||||||
|
},
|
||||||
|
"sync"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await config.beforeBuild?.(app);
|
||||||
|
await app.build(config.buildConfig);
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createRuntimeApp<Env = any>(
|
||||||
|
{
|
||||||
|
serveStatic,
|
||||||
|
registerLocalMedia,
|
||||||
|
adminOptions,
|
||||||
|
...config
|
||||||
|
}: RuntimeBkndConfig & {
|
||||||
|
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
|
||||||
|
registerLocalMedia?: boolean;
|
||||||
|
adminOptions?: AdminControllerOptions | false;
|
||||||
|
},
|
||||||
|
env?: Env
|
||||||
|
): Promise<App> {
|
||||||
|
if (registerLocalMedia) {
|
||||||
|
registerLocalMediaAdapter();
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = App.create(makeConfig(config, env));
|
||||||
|
|
||||||
|
app.emgr.onEvent(
|
||||||
|
App.Events.AppBuiltEvent,
|
||||||
|
async () => {
|
||||||
|
if (serveStatic) {
|
||||||
|
if (Array.isArray(serveStatic)) {
|
||||||
|
const [path, handler] = serveStatic;
|
||||||
|
app.modules.server.get(path, handler);
|
||||||
|
} else {
|
||||||
|
app.modules.server.get("/*", serveStatic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await config.onBuilt?.(app);
|
||||||
|
if (adminOptions !== false) {
|
||||||
|
app.registerAdminController(adminOptions);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sync"
|
||||||
|
);
|
||||||
|
|
||||||
|
await config.beforeBuild?.(app);
|
||||||
|
await app.build(config.buildConfig);
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||||
import { Api, App, type CreateAppConfig } from "bknd";
|
import { Api, type App } from "bknd";
|
||||||
import { nodeRequestToRequest } from "../index";
|
import { type FrameworkBkndConfig, createFrameworkApp, nodeRequestToRequest } from "../index";
|
||||||
|
|
||||||
|
export type NextjsBkndConfig = FrameworkBkndConfig;
|
||||||
|
|
||||||
type GetServerSidePropsContext = {
|
type GetServerSidePropsContext = {
|
||||||
req: IncomingMessage;
|
req: IncomingMessage;
|
||||||
@@ -18,7 +20,6 @@ type GetServerSidePropsContext = {
|
|||||||
|
|
||||||
export function createApi({ req }: GetServerSidePropsContext) {
|
export function createApi({ req }: GetServerSidePropsContext) {
|
||||||
const request = nodeRequestToRequest(req);
|
const request = nodeRequestToRequest(req);
|
||||||
//console.log("createApi:request.headers", request.headers);
|
|
||||||
return new Api({
|
return new Api({
|
||||||
host: new URL(request.url).origin,
|
host: new URL(request.url).origin,
|
||||||
headers: request.headers
|
headers: request.headers
|
||||||
@@ -43,11 +44,10 @@ function getCleanRequest(req: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let app: App;
|
let app: App;
|
||||||
export function serve(config: CreateAppConfig) {
|
export function serve(config: NextjsBkndConfig = {}) {
|
||||||
return async (req: Request) => {
|
return async (req: Request) => {
|
||||||
if (!app) {
|
if (!app) {
|
||||||
app = App.create(config);
|
app = await createFrameworkApp(config);
|
||||||
await app.build();
|
|
||||||
}
|
}
|
||||||
const request = getCleanRequest(req);
|
const request = getCleanRequest(req);
|
||||||
return app.fetch(request, process.env);
|
return app.fetch(request, process.env);
|
||||||
|
|||||||
@@ -1,59 +1,6 @@
|
|||||||
import path from "node:path";
|
export * from "./node.adapter";
|
||||||
import { serve as honoServe } from "@hono/node-server";
|
export {
|
||||||
import { serveStatic } from "@hono/node-server/serve-static";
|
StorageLocalAdapter,
|
||||||
import { App, type CreateAppConfig } from "bknd";
|
type LocalAdapterConfig
|
||||||
|
} from "../../media/storage/adapters/StorageLocalAdapter";
|
||||||
export type NodeAdapterOptions = CreateAppConfig & {
|
export { registerLocalMediaAdapter } from "../index";
|
||||||
relativeDistPath?: string;
|
|
||||||
port?: number;
|
|
||||||
hostname?: string;
|
|
||||||
listener?: Parameters<typeof honoServe>[1];
|
|
||||||
};
|
|
||||||
|
|
||||||
export function serve({
|
|
||||||
relativeDistPath,
|
|
||||||
port = 1337,
|
|
||||||
hostname,
|
|
||||||
listener,
|
|
||||||
...config
|
|
||||||
}: NodeAdapterOptions = {}) {
|
|
||||||
const root = path.relative(
|
|
||||||
process.cwd(),
|
|
||||||
path.resolve(relativeDistPath ?? "./node_modules/bknd/dist", "static")
|
|
||||||
);
|
|
||||||
let app: App;
|
|
||||||
|
|
||||||
honoServe(
|
|
||||||
{
|
|
||||||
port,
|
|
||||||
hostname,
|
|
||||||
fetch: async (req: Request) => {
|
|
||||||
if (!app) {
|
|
||||||
app = App.create(config);
|
|
||||||
|
|
||||||
app.emgr.on(
|
|
||||||
"app-built",
|
|
||||||
async () => {
|
|
||||||
app.modules.server.get(
|
|
||||||
"/*",
|
|
||||||
serveStatic({
|
|
||||||
root
|
|
||||||
})
|
|
||||||
);
|
|
||||||
app.registerAdminController();
|
|
||||||
},
|
|
||||||
"sync"
|
|
||||||
);
|
|
||||||
|
|
||||||
await app.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
return app.fetch(req);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(connInfo) => {
|
|
||||||
console.log(`Server is running on http://localhost:${connInfo.port}`);
|
|
||||||
listener?.(connInfo);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
58
app/src/adapter/node/node.adapter.ts
Normal file
58
app/src/adapter/node/node.adapter.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { serve as honoServe } from "@hono/node-server";
|
||||||
|
import { serveStatic } from "@hono/node-server/serve-static";
|
||||||
|
import type { App } from "bknd";
|
||||||
|
import { config as $config } from "core";
|
||||||
|
import { type RuntimeBkndConfig, createRuntimeApp } from "../index";
|
||||||
|
|
||||||
|
export type NodeBkndConfig = RuntimeBkndConfig & {
|
||||||
|
port?: number;
|
||||||
|
hostname?: string;
|
||||||
|
listener?: Parameters<typeof honoServe>[1];
|
||||||
|
/** @deprecated */
|
||||||
|
relativeDistPath?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function serve({
|
||||||
|
distPath,
|
||||||
|
relativeDistPath,
|
||||||
|
port = $config.server.default_port,
|
||||||
|
hostname,
|
||||||
|
listener,
|
||||||
|
onBuilt,
|
||||||
|
buildConfig = {},
|
||||||
|
beforeBuild,
|
||||||
|
...config
|
||||||
|
}: NodeBkndConfig = {}) {
|
||||||
|
const root = path.relative(
|
||||||
|
process.cwd(),
|
||||||
|
path.resolve(distPath ?? relativeDistPath ?? "./node_modules/bknd/dist", "static")
|
||||||
|
);
|
||||||
|
if (relativeDistPath) {
|
||||||
|
console.warn("relativeDistPath is deprecated, please use distPath instead");
|
||||||
|
}
|
||||||
|
|
||||||
|
let app: App;
|
||||||
|
|
||||||
|
honoServe(
|
||||||
|
{
|
||||||
|
port,
|
||||||
|
hostname,
|
||||||
|
fetch: async (req: Request) => {
|
||||||
|
if (!app) {
|
||||||
|
app = await createRuntimeApp({
|
||||||
|
...config,
|
||||||
|
registerLocalMedia: true,
|
||||||
|
serveStatic: serveStatic({ root })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.fetch(req);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(connInfo) => {
|
||||||
|
console.log(`Server is running on http://localhost:${connInfo.port}`);
|
||||||
|
listener?.(connInfo);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import { App, type CreateAppConfig } from "bknd";
|
import { type FrameworkBkndConfig, createFrameworkApp } from "adapter";
|
||||||
|
import type { App } from "bknd";
|
||||||
|
|
||||||
|
export type RemixBkndConfig = FrameworkBkndConfig;
|
||||||
|
|
||||||
let app: App;
|
let app: App;
|
||||||
export function serve(config: CreateAppConfig) {
|
export function serve(config: RemixBkndConfig = {}) {
|
||||||
return async (args: { request: Request }) => {
|
return async (args: { request: Request }) => {
|
||||||
if (!app) {
|
if (!app) {
|
||||||
app = App.create(config);
|
app = await createFrameworkApp(config);
|
||||||
await app.build();
|
|
||||||
}
|
}
|
||||||
return app.fetch(args.request);
|
return app.fetch(args.request);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,47 +1,57 @@
|
|||||||
import { serveStatic } from "@hono/node-server/serve-static";
|
import { serveStatic } from "@hono/node-server/serve-static";
|
||||||
import type { BkndConfig } from "bknd";
|
import { type RuntimeBkndConfig, createRuntimeApp } from "adapter";
|
||||||
import { App } from "bknd";
|
import type { App } from "bknd";
|
||||||
|
|
||||||
function createApp(config: BkndConfig, env: any) {
|
export type ViteBkndConfig<Env = any> = RuntimeBkndConfig<Env> & {
|
||||||
const create_config = typeof config.app === "function" ? config.app(env) : config.app;
|
setAdminHtml?: boolean;
|
||||||
return App.create(create_config);
|
forceDev?: boolean;
|
||||||
}
|
html?: string;
|
||||||
|
};
|
||||||
|
|
||||||
function setAppBuildListener(app: App, config: BkndConfig, html?: string) {
|
export function addViteScript(html: string, addBkndContext: boolean = true) {
|
||||||
app.emgr.on(
|
return html.replace(
|
||||||
"app-built",
|
"</head>",
|
||||||
async () => {
|
`<script type="module">
|
||||||
await config.onBuilt?.(app);
|
import RefreshRuntime from "/@react-refresh"
|
||||||
if (config.setAdminHtml) {
|
RefreshRuntime.injectIntoGlobalHook(window)
|
||||||
app.registerAdminController({ html, forceDev: true });
|
window.$RefreshReg$ = () => {}
|
||||||
app.module.server.client.get("/assets/*", serveStatic({ root: "./" }));
|
window.$RefreshSig$ = () => (type) => type
|
||||||
}
|
window.__vite_plugin_react_preamble_installed__ = true
|
||||||
},
|
</script>
|
||||||
"sync"
|
<script type="module" src="/@vite/client"></script>
|
||||||
|
${addBkndContext ? "<!-- BKND_CONTEXT -->" : ""}
|
||||||
|
</head>`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function serveFresh(config: BkndConfig, _html?: string) {
|
async function createApp(config: ViteBkndConfig, env?: any) {
|
||||||
|
return await createRuntimeApp(
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
adminOptions: config.setAdminHtml
|
||||||
|
? { html: config.html, forceDev: config.forceDev }
|
||||||
|
: undefined,
|
||||||
|
serveStatic: ["/assets/*", serveStatic({ root: config.distPath ?? "./" })]
|
||||||
|
},
|
||||||
|
env
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function serveFresh(config: ViteBkndConfig) {
|
||||||
return {
|
return {
|
||||||
async fetch(request: Request, env: any, ctx: ExecutionContext) {
|
async fetch(request: Request, env: any, ctx: ExecutionContext) {
|
||||||
const app = createApp(config, env);
|
const app = await createApp(config, env);
|
||||||
|
|
||||||
setAppBuildListener(app, config, _html);
|
|
||||||
await app.build();
|
|
||||||
|
|
||||||
return app.fetch(request, env, ctx);
|
return app.fetch(request, env, ctx);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let app: App;
|
let app: App;
|
||||||
export async function serveCached(config: BkndConfig, _html?: string) {
|
export async function serveCached(config: ViteBkndConfig) {
|
||||||
return {
|
return {
|
||||||
async fetch(request: Request, env: any, ctx: ExecutionContext) {
|
async fetch(request: Request, env: any, ctx: ExecutionContext) {
|
||||||
if (!app) {
|
if (!app) {
|
||||||
app = createApp(config, env);
|
app = await createApp(config, env);
|
||||||
setAppBuildListener(app, config, _html);
|
|
||||||
await app.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return app.fetch(request, env, ctx);
|
return app.fetch(request, env, ctx);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { type AuthAction, Authenticator, type ProfileExchange, Role, type Strategy } from "auth";
|
import { type AuthAction, Authenticator, type ProfileExchange, Role, type Strategy } from "auth";
|
||||||
import { Exception } from "core";
|
import type { PasswordStrategy } from "auth/authenticate/strategies";
|
||||||
|
import { Exception, type PrimaryFieldType } from "core";
|
||||||
import { type Static, secureRandomString, transformObject } from "core/utils";
|
import { type Static, secureRandomString, transformObject } from "core/utils";
|
||||||
import { type Entity, EntityIndex, type EntityManager } from "data";
|
import { type Entity, EntityIndex, type EntityManager } from "data";
|
||||||
import { type FieldSchema, entity, enumm, make, text } from "data/prototype";
|
import { type FieldSchema, entity, enumm, make, text } from "data/prototype";
|
||||||
@@ -9,9 +10,9 @@ import { AuthController } from "./api/AuthController";
|
|||||||
import { type AppAuthSchema, STRATEGIES, authConfigSchema } from "./auth-schema";
|
import { type AppAuthSchema, STRATEGIES, authConfigSchema } from "./auth-schema";
|
||||||
|
|
||||||
export type UserFieldSchema = FieldSchema<typeof AppAuth.usersFields>;
|
export type UserFieldSchema = FieldSchema<typeof AppAuth.usersFields>;
|
||||||
declare global {
|
declare module "core" {
|
||||||
interface DB {
|
interface DB {
|
||||||
users: UserFieldSchema;
|
users: { id: PrimaryFieldType } & UserFieldSchema;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +101,7 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
|||||||
return this._authenticator!;
|
return this._authenticator!;
|
||||||
}
|
}
|
||||||
|
|
||||||
get em(): EntityManager<DB> {
|
get em(): EntityManager {
|
||||||
return this.ctx.em as any;
|
return this.ctx.em as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +161,9 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
|||||||
|
|
||||||
const users = this.getUsersEntity();
|
const users = this.getUsersEntity();
|
||||||
this.toggleStrategyValueVisibility(true);
|
this.toggleStrategyValueVisibility(true);
|
||||||
const result = await this.em.repo(users).findOne({ email: profile.email! });
|
const result = await this.em
|
||||||
|
.repo(users as unknown as "users")
|
||||||
|
.findOne({ email: profile.email! });
|
||||||
this.toggleStrategyValueVisibility(false);
|
this.toggleStrategyValueVisibility(false);
|
||||||
if (!result.data) {
|
if (!result.data) {
|
||||||
throw new Exception("User not found", 404);
|
throw new Exception("User not found", 404);
|
||||||
@@ -197,7 +200,7 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
|||||||
throw new Exception("User already exists");
|
throw new Exception("User already exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload: any = {
|
||||||
...profile,
|
...profile,
|
||||||
strategy: strategy.getName(),
|
strategy: strategy.getName(),
|
||||||
strategy_value: identifier
|
strategy_value: identifier
|
||||||
@@ -284,6 +287,25 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createUser({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
...additional
|
||||||
|
}: { email: string; password: string; [key: string]: any }) {
|
||||||
|
const strategy = "password";
|
||||||
|
const pw = this.authenticator.strategy(strategy) as PasswordStrategy;
|
||||||
|
const strategy_value = await pw.hash(password);
|
||||||
|
const mutator = this.em.mutator(this.config.entity_name as "users");
|
||||||
|
mutator.__unstable_toggleSystemEntityCreation(false);
|
||||||
|
const { data: created } = await mutator.insertOne({
|
||||||
|
...(additional as any),
|
||||||
|
strategy,
|
||||||
|
strategy_value
|
||||||
|
});
|
||||||
|
mutator.__unstable_toggleSystemEntityCreation(true);
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
override toJSON(secrets?: boolean): AppAuthSchema {
|
override toJSON(secrets?: boolean): AppAuthSchema {
|
||||||
if (!this.config.enabled) {
|
if (!this.config.enabled) {
|
||||||
return this.configDefault;
|
return this.configDefault;
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getAuthCookie(c: Context): Promise<string | undefined> {
|
private async getAuthCookie(c: Context): Promise<string | undefined> {
|
||||||
|
try {
|
||||||
const secret = this.config.jwt.secret;
|
const secret = this.config.jwt.secret;
|
||||||
|
|
||||||
const token = await getSignedCookie(c, secret, "auth");
|
const token = await getSignedCookie(c, secret, "auth");
|
||||||
@@ -229,6 +230,13 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
|||||||
}
|
}
|
||||||
|
|
||||||
return token;
|
return token;
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
console.error("[Error:getAuthCookie]", e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async requestCookieRefresh(c: Context) {
|
async requestCookieRefresh(c: Context) {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import type { Config } from "@libsql/client/node";
|
import type { Config } from "@libsql/client/node";
|
||||||
import { App, type CreateAppConfig } from "App";
|
import { App, type CreateAppConfig } from "App";
|
||||||
import type { BkndConfig } from "adapter";
|
import { StorageLocalAdapter } from "adapter/node";
|
||||||
import type { CliCommand } from "cli/types";
|
import type { CliBkndConfig, CliCommand } from "cli/types";
|
||||||
import { Option } from "commander";
|
import { Option } from "commander";
|
||||||
|
import { config } from "core";
|
||||||
|
import { registries } from "modules/registries";
|
||||||
import {
|
import {
|
||||||
PLATFORMS,
|
PLATFORMS,
|
||||||
type Platform,
|
type Platform,
|
||||||
@@ -19,7 +21,7 @@ export const run: CliCommand = (program) => {
|
|||||||
.addOption(
|
.addOption(
|
||||||
new Option("-p, --port <port>", "port to run on")
|
new Option("-p, --port <port>", "port to run on")
|
||||||
.env("PORT")
|
.env("PORT")
|
||||||
.default(1337)
|
.default(config.server.default_port)
|
||||||
.argParser((v) => Number.parseInt(v))
|
.argParser((v) => Number.parseInt(v))
|
||||||
)
|
)
|
||||||
.addOption(new Option("-c, --config <config>", "config file"))
|
.addOption(new Option("-c, --config <config>", "config file"))
|
||||||
@@ -37,6 +39,12 @@ export const run: CliCommand = (program) => {
|
|||||||
.action(action);
|
.action(action);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// automatically register local adapter
|
||||||
|
const local = StorageLocalAdapter.prototype.getName();
|
||||||
|
if (!registries.media.has(local)) {
|
||||||
|
registries.media.register(local, StorageLocalAdapter);
|
||||||
|
}
|
||||||
|
|
||||||
type MakeAppConfig = {
|
type MakeAppConfig = {
|
||||||
connection?: CreateAppConfig["connection"];
|
connection?: CreateAppConfig["connection"];
|
||||||
server?: { platform?: Platform };
|
server?: { platform?: Platform };
|
||||||
@@ -47,8 +55,8 @@ type MakeAppConfig = {
|
|||||||
async function makeApp(config: MakeAppConfig) {
|
async function makeApp(config: MakeAppConfig) {
|
||||||
const app = App.create({ connection: config.connection });
|
const app = App.create({ connection: config.connection });
|
||||||
|
|
||||||
app.emgr.on(
|
app.emgr.onEvent(
|
||||||
"app-built",
|
App.Events.AppBuiltEvent,
|
||||||
async () => {
|
async () => {
|
||||||
await attachServeStatic(app, config.server?.platform ?? "node");
|
await attachServeStatic(app, config.server?.platform ?? "node");
|
||||||
app.registerAdminController();
|
app.registerAdminController();
|
||||||
@@ -64,24 +72,23 @@ async function makeApp(config: MakeAppConfig) {
|
|||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function makeConfigApp(config: BkndConfig, platform?: Platform) {
|
export async function makeConfigApp(config: CliBkndConfig, platform?: Platform) {
|
||||||
const appConfig = typeof config.app === "function" ? config.app(process.env) : config.app;
|
const appConfig = typeof config.app === "function" ? config.app(process.env) : config.app;
|
||||||
const app = App.create(appConfig);
|
const app = App.create(appConfig);
|
||||||
|
|
||||||
app.emgr.on(
|
app.emgr.onEvent(
|
||||||
"app-built",
|
App.Events.AppBuiltEvent,
|
||||||
async () => {
|
async () => {
|
||||||
await attachServeStatic(app, platform ?? "node");
|
await attachServeStatic(app, platform ?? "node");
|
||||||
app.registerAdminController();
|
app.registerAdminController();
|
||||||
|
|
||||||
if (config.onBuilt) {
|
await config.onBuilt?.(app);
|
||||||
await config.onBuilt(app);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"sync"
|
"sync"
|
||||||
);
|
);
|
||||||
|
|
||||||
await app.build();
|
await config.beforeBuild?.(app);
|
||||||
|
await app.build(config.buildConfig);
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +109,7 @@ async function action(options: {
|
|||||||
app = await makeApp({ connection, server: { platform: options.server } });
|
app = await makeApp({ connection, server: { platform: options.server } });
|
||||||
} else {
|
} else {
|
||||||
console.log("Using config from:", configFilePath);
|
console.log("Using config from:", configFilePath);
|
||||||
const config = (await import(configFilePath).then((m) => m.default)) as BkndConfig;
|
const config = (await import(configFilePath).then((m) => m.default)) as CliBkndConfig;
|
||||||
app = await makeConfigApp(config, options.server);
|
app = await makeConfigApp(config, options.server);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { password as $password, text as $text } from "@clack/prompts";
|
import { password as $password, text as $text } from "@clack/prompts";
|
||||||
|
import type { App } from "App";
|
||||||
import type { PasswordStrategy } from "auth/authenticate/strategies";
|
import type { PasswordStrategy } from "auth/authenticate/strategies";
|
||||||
import type { App, BkndConfig } from "bknd";
|
|
||||||
import { makeConfigApp } from "cli/commands/run";
|
import { makeConfigApp } from "cli/commands/run";
|
||||||
import { getConfigPath } from "cli/commands/run/platform";
|
import { getConfigPath } from "cli/commands/run/platform";
|
||||||
import type { CliCommand } from "cli/types";
|
import type { CliBkndConfig, CliCommand } from "cli/types";
|
||||||
import { Argument } from "commander";
|
import { Argument } from "commander";
|
||||||
|
|
||||||
export const user: CliCommand = (program) => {
|
export const user: CliCommand = (program) => {
|
||||||
@@ -21,7 +21,7 @@ async function action(action: "create" | "update", options: any) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = (await import(configFilePath).then((m) => m.default)) as BkndConfig;
|
const config = (await import(configFilePath).then((m) => m.default)) as CliBkndConfig;
|
||||||
const app = await makeConfigApp(config, options.server);
|
const app = await makeConfigApp(config, options.server);
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
@@ -37,7 +37,7 @@ async function action(action: "create" | "update", options: any) {
|
|||||||
async function create(app: App, options: any) {
|
async function create(app: App, options: any) {
|
||||||
const config = app.module.auth.toJSON(true);
|
const config = app.module.auth.toJSON(true);
|
||||||
const strategy = app.module.auth.authenticator.strategy("password") as PasswordStrategy;
|
const strategy = app.module.auth.authenticator.strategy("password") as PasswordStrategy;
|
||||||
const users_entity = config.entity_name;
|
const users_entity = config.entity_name as "users";
|
||||||
|
|
||||||
const email = await $text({
|
const email = await $text({
|
||||||
message: "Enter email",
|
message: "Enter email",
|
||||||
@@ -83,7 +83,7 @@ async function create(app: App, options: any) {
|
|||||||
async function update(app: App, options: any) {
|
async function update(app: App, options: any) {
|
||||||
const config = app.module.auth.toJSON(true);
|
const config = app.module.auth.toJSON(true);
|
||||||
const strategy = app.module.auth.authenticator.strategy("password") as PasswordStrategy;
|
const strategy = app.module.auth.authenticator.strategy("password") as PasswordStrategy;
|
||||||
const users_entity = config.entity_name;
|
const users_entity = config.entity_name as "users";
|
||||||
const em = app.modules.ctx().em;
|
const em = app.modules.ctx().em;
|
||||||
|
|
||||||
const email = (await $text({
|
const email = (await $text({
|
||||||
|
|||||||
11
app/src/cli/types.d.ts
vendored
11
app/src/cli/types.d.ts
vendored
@@ -1,3 +1,14 @@
|
|||||||
|
import type { CreateAppConfig } from "App";
|
||||||
|
import type { FrameworkBkndConfig } from "adapter";
|
||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
|
|
||||||
export type CliCommand = (program: Command) => void;
|
export type CliCommand = (program: Command) => void;
|
||||||
|
|
||||||
|
export type CliBkndConfig<Env = any> = FrameworkBkndConfig & {
|
||||||
|
app: CreateAppConfig | ((env: Env) => CreateAppConfig);
|
||||||
|
setAdminHtml?: boolean;
|
||||||
|
server?: {
|
||||||
|
port?: number;
|
||||||
|
platform?: "node" | "bun";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,7 +5,13 @@ import type { Generated } from "kysely";
|
|||||||
|
|
||||||
export type PrimaryFieldType = number | Generated<number>;
|
export type PrimaryFieldType = number | Generated<number>;
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/noEmptyInterface: <explanation>
|
||||||
|
export interface DB {}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
|
server: {
|
||||||
|
default_port: 1337
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
default_primary_field: "id"
|
default_primary_field: "id"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export class EventManager<
|
|||||||
> {
|
> {
|
||||||
protected events: EventClass[] = [];
|
protected events: EventClass[] = [];
|
||||||
protected listeners: EventListener[] = [];
|
protected listeners: EventListener[] = [];
|
||||||
|
enabled: boolean = true;
|
||||||
|
|
||||||
constructor(events?: RegisteredEvents, listeners?: EventListener[]) {
|
constructor(events?: RegisteredEvents, listeners?: EventListener[]) {
|
||||||
if (events) {
|
if (events) {
|
||||||
@@ -28,6 +29,16 @@ export class EventManager<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enable() {
|
||||||
|
this.enabled = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
this.enabled = false;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
clearEvents() {
|
clearEvents() {
|
||||||
this.events = [];
|
this.events = [];
|
||||||
return this;
|
return this;
|
||||||
@@ -39,6 +50,10 @@ export class EventManager<
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getListeners(): EventListener[] {
|
||||||
|
return [...this.listeners];
|
||||||
|
}
|
||||||
|
|
||||||
get Events(): { [K in keyof RegisteredEvents]: RegisteredEvents[K] } {
|
get Events(): { [K in keyof RegisteredEvents]: RegisteredEvents[K] } {
|
||||||
// proxy class to access events
|
// proxy class to access events
|
||||||
return new Proxy(this, {
|
return new Proxy(this, {
|
||||||
@@ -133,6 +148,11 @@ export class EventManager<
|
|||||||
async emit(event: Event) {
|
async emit(event: Event) {
|
||||||
// @ts-expect-error slug is static
|
// @ts-expect-error slug is static
|
||||||
const slug = event.constructor.slug;
|
const slug = event.constructor.slug;
|
||||||
|
if (!this.enabled) {
|
||||||
|
console.log("EventManager disabled, not emitting", slug);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.eventExists(event)) {
|
if (!this.eventExists(event)) {
|
||||||
throw new Error(`Event "${slug}" not registered`);
|
throw new Error(`Event "${slug}" not registered`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Hono, MiddlewareHandler } from "hono";
|
|||||||
export { tbValidator } from "./server/lib/tbValidator";
|
export { tbValidator } from "./server/lib/tbValidator";
|
||||||
export { Exception, BkndError } from "./errors";
|
export { Exception, BkndError } from "./errors";
|
||||||
export { isDebug } from "./env";
|
export { isDebug } from "./env";
|
||||||
export { type PrimaryFieldType, config } from "./config";
|
export { type PrimaryFieldType, config, type DB } from "./config";
|
||||||
export { AwsClient } from "./clients/aws/AwsClient";
|
export { AwsClient } from "./clients/aws/AwsClient";
|
||||||
export {
|
export {
|
||||||
SimpleRenderer,
|
SimpleRenderer,
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ export class SchemaObject<Schema extends TObject> {
|
|||||||
forceParse: true,
|
forceParse: true,
|
||||||
skipMark: this.isForceParse()
|
skipMark: this.isForceParse()
|
||||||
});
|
});
|
||||||
const updatedConfig = noEmit ? valid : await this.onBeforeUpdate(this._config, valid);
|
// regardless of "noEmit" – this should always be triggered
|
||||||
|
const updatedConfig = await this.onBeforeUpdate(this._config, valid);
|
||||||
|
|
||||||
this._value = updatedConfig;
|
this._value = updatedConfig;
|
||||||
this._config = Object.freeze(updatedConfig);
|
this._config = Object.freeze(updatedConfig);
|
||||||
|
|||||||
@@ -1,29 +1,50 @@
|
|||||||
export type Constructor<T> = new (...args: any[]) => T;
|
export type Constructor<T> = new (...args: any[]) => T;
|
||||||
export class Registry<Item, Items extends Record<string, object> = Record<string, object>> {
|
|
||||||
|
export type RegisterFn<Item> = (unknown: any) => Item;
|
||||||
|
|
||||||
|
export class Registry<
|
||||||
|
Item,
|
||||||
|
Items extends Record<string, Item> = Record<string, Item>,
|
||||||
|
Fn extends RegisterFn<Item> = RegisterFn<Item>
|
||||||
|
> {
|
||||||
private is_set: boolean = false;
|
private is_set: boolean = false;
|
||||||
private items: Items = {} as Items;
|
private items: Items = {} as Items;
|
||||||
|
|
||||||
set<Actual extends Record<string, object>>(items: Actual) {
|
constructor(private registerFn?: Fn) {}
|
||||||
|
|
||||||
|
set<Actual extends Record<string, Item>>(items: Actual) {
|
||||||
if (this.is_set) {
|
if (this.is_set) {
|
||||||
throw new Error("Registry is already set");
|
throw new Error("Registry is already set");
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
this.items = items as unknown as Items;
|
||||||
this.items = items;
|
|
||||||
this.is_set = true;
|
this.is_set = true;
|
||||||
|
|
||||||
return this as unknown as Registry<Item, Actual>;
|
return this as unknown as Registry<Item, Actual, Fn>;
|
||||||
}
|
}
|
||||||
|
|
||||||
add(name: string, item: Item) {
|
add(name: string, item: Item) {
|
||||||
// @ts-ignore
|
this.items[name as keyof Items] = item as Items[keyof Items];
|
||||||
this.items[name] = item;
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
register(name: string, specific: Parameters<Fn>[0]) {
|
||||||
|
if (this.registerFn) {
|
||||||
|
const item = this.registerFn(specific);
|
||||||
|
this.items[name as keyof Items] = item as Items[keyof Items];
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.add(name, specific);
|
||||||
|
}
|
||||||
|
|
||||||
get<Name extends keyof Items>(name: Name): Items[Name] {
|
get<Name extends keyof Items>(name: Name): Items[Name] {
|
||||||
return this.items[name];
|
return this.items[name];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
has(name: keyof Items): boolean {
|
||||||
|
return name in this.items;
|
||||||
|
}
|
||||||
|
|
||||||
all() {
|
all() {
|
||||||
return this.items;
|
return this.items;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,16 @@ export class DebugLogger {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.last = 0;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
log(...args: any[]) {
|
log(...args: any[]) {
|
||||||
if (!this._enabled) return this;
|
if (!this._enabled) return this;
|
||||||
|
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
const time = Number.parseInt(String(now - this.last));
|
const time = this.last === 0 ? 0 : Number.parseInt(String(now - this.last));
|
||||||
const indents = " ".repeat(this._context.length);
|
const indents = " ".repeat(this._context.length);
|
||||||
const context =
|
const context =
|
||||||
this._context.length > 0 ? `[${this._context[this._context.length - 1]}]` : "";
|
this._context.length > 0 ? `[${this._context[this._context.length - 1]}]` : "";
|
||||||
|
|||||||
@@ -9,10 +9,25 @@ export async function withDisabledConsole<R>(
|
|||||||
fn: () => Promise<R>,
|
fn: () => Promise<R>,
|
||||||
severities: ConsoleSeverity[] = ["log"]
|
severities: ConsoleSeverity[] = ["log"]
|
||||||
): Promise<R> {
|
): Promise<R> {
|
||||||
const enable = disableConsoleLog(severities);
|
const _oldConsoles = {
|
||||||
|
log: console.log,
|
||||||
|
warn: console.warn,
|
||||||
|
error: console.error
|
||||||
|
};
|
||||||
|
disableConsoleLog(severities);
|
||||||
|
const enable = () => {
|
||||||
|
Object.entries(_oldConsoles).forEach(([severity, fn]) => {
|
||||||
|
console[severity as ConsoleSeverity] = fn;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
try {
|
||||||
const result = await fn();
|
const result = await fn();
|
||||||
enable();
|
enable();
|
||||||
return result;
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
enable();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function disableConsoleLog(severities: ConsoleSeverity[] = ["log"]) {
|
export function disableConsoleLog(severities: ConsoleSeverity[] = ["log"]) {
|
||||||
|
|||||||
@@ -1,52 +1,20 @@
|
|||||||
import { transformObject } from "core/utils";
|
import { transformObject } from "core/utils";
|
||||||
import { DataPermissions, Entity, EntityIndex, type EntityManager, type Field } from "data";
|
import {
|
||||||
|
DataPermissions,
|
||||||
|
type Entity,
|
||||||
|
EntityIndex,
|
||||||
|
type EntityManager,
|
||||||
|
constructEntity,
|
||||||
|
constructRelation
|
||||||
|
} from "data";
|
||||||
import { Module } from "modules/Module";
|
import { Module } from "modules/Module";
|
||||||
import { DataController } from "./api/DataController";
|
import { DataController } from "./api/DataController";
|
||||||
import {
|
import { type AppDataConfig, dataConfigSchema } from "./data-schema";
|
||||||
type AppDataConfig,
|
|
||||||
FIELDS,
|
|
||||||
RELATIONS,
|
|
||||||
type TAppDataEntity,
|
|
||||||
type TAppDataRelation,
|
|
||||||
dataConfigSchema
|
|
||||||
} from "./data-schema";
|
|
||||||
|
|
||||||
export class AppData<DB> extends Module<typeof dataConfigSchema> {
|
|
||||||
static constructEntity(name: string, entityConfig: TAppDataEntity) {
|
|
||||||
const fields = transformObject(entityConfig.fields ?? {}, (fieldConfig, name) => {
|
|
||||||
const { type } = fieldConfig;
|
|
||||||
if (!(type in FIELDS)) {
|
|
||||||
throw new Error(`Field type "${type}" not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { field } = FIELDS[type as any];
|
|
||||||
const returnal = new field(name, fieldConfig.config) as Field;
|
|
||||||
return returnal;
|
|
||||||
});
|
|
||||||
|
|
||||||
// @todo: entity must be migrated to typebox
|
|
||||||
return new Entity(
|
|
||||||
name,
|
|
||||||
Object.values(fields),
|
|
||||||
entityConfig.config as any,
|
|
||||||
entityConfig.type as any
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static constructRelation(
|
|
||||||
relationConfig: TAppDataRelation,
|
|
||||||
resolver: (name: Entity | string) => Entity
|
|
||||||
) {
|
|
||||||
return new RELATIONS[relationConfig.type].cls(
|
|
||||||
resolver(relationConfig.source),
|
|
||||||
resolver(relationConfig.target),
|
|
||||||
relationConfig.config
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export class AppData extends Module<typeof dataConfigSchema> {
|
||||||
override async build() {
|
override async build() {
|
||||||
const entities = transformObject(this.config.entities ?? {}, (entityConfig, name) => {
|
const entities = transformObject(this.config.entities ?? {}, (entityConfig, name) => {
|
||||||
return AppData.constructEntity(name, entityConfig);
|
return constructEntity(name, entityConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
const _entity = (_e: Entity | string): Entity => {
|
const _entity = (_e: Entity | string): Entity => {
|
||||||
@@ -57,7 +25,7 @@ export class AppData<DB> extends Module<typeof dataConfigSchema> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const relations = transformObject(this.config.relations ?? {}, (relation) =>
|
const relations = transformObject(this.config.relations ?? {}, (relation) =>
|
||||||
AppData.constructRelation(relation, _entity)
|
constructRelation(relation, _entity)
|
||||||
);
|
);
|
||||||
|
|
||||||
const indices = transformObject(this.config.indices ?? {}, (index, name) => {
|
const indices = transformObject(this.config.indices ?? {}, (index, name) => {
|
||||||
@@ -91,7 +59,7 @@ export class AppData<DB> extends Module<typeof dataConfigSchema> {
|
|||||||
return dataConfigSchema;
|
return dataConfigSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
get em(): EntityManager<DB> {
|
get em(): EntityManager {
|
||||||
this.throwIfNotBuilt();
|
this.throwIfNotBuilt();
|
||||||
return this.ctx.em;
|
return this.ctx.em;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { DB } from "core";
|
||||||
import type { EntityData, RepoQuery, RepositoryResponse } from "data";
|
import type { EntityData, RepoQuery, RepositoryResponse } from "data";
|
||||||
import { type BaseModuleApiOptions, ModuleApi, type PrimaryFieldType } from "modules";
|
import { type BaseModuleApiOptions, ModuleApi, type PrimaryFieldType } from "modules";
|
||||||
|
|
||||||
@@ -15,48 +16,60 @@ export class DataApi extends ModuleApi<DataApiOptions> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
readOne(
|
readOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||||
entity: string,
|
entity: E,
|
||||||
id: PrimaryFieldType,
|
id: PrimaryFieldType,
|
||||||
query: Partial<Omit<RepoQuery, "where" | "limit" | "offset">> = {}
|
query: Partial<Omit<RepoQuery, "where" | "limit" | "offset">> = {}
|
||||||
) {
|
) {
|
||||||
return this.get<RepositoryResponse<EntityData>>([entity, id], query);
|
return this.get<Pick<RepositoryResponse<Data>, "meta" | "data">>([entity as any, id], query);
|
||||||
}
|
}
|
||||||
|
|
||||||
readMany(entity: string, query: Partial<RepoQuery> = {}) {
|
readMany<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||||
return this.get<Pick<RepositoryResponse, "meta" | "data">>(
|
entity: E,
|
||||||
[entity],
|
|
||||||
query ?? this.options.defaultQuery
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
readManyByReference(
|
|
||||||
entity: string,
|
|
||||||
id: PrimaryFieldType,
|
|
||||||
reference: string,
|
|
||||||
query: Partial<RepoQuery> = {}
|
query: Partial<RepoQuery> = {}
|
||||||
) {
|
) {
|
||||||
return this.get<Pick<RepositoryResponse, "meta" | "data">>(
|
return this.get<Pick<RepositoryResponse<Data[]>, "meta" | "data">>(
|
||||||
[entity, id, reference],
|
[entity as any],
|
||||||
query ?? this.options.defaultQuery
|
query ?? this.options.defaultQuery
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
createOne(entity: string, input: EntityData) {
|
readManyByReference<
|
||||||
return this.post<RepositoryResponse<EntityData>>([entity], input);
|
E extends keyof DB | string,
|
||||||
|
R extends keyof DB | string,
|
||||||
|
Data = R extends keyof DB ? DB[R] : EntityData
|
||||||
|
>(entity: E, id: PrimaryFieldType, reference: R, query: Partial<RepoQuery> = {}) {
|
||||||
|
return this.get<Pick<RepositoryResponse<Data[]>, "meta" | "data">>(
|
||||||
|
[entity as any, id, reference],
|
||||||
|
query ?? this.options.defaultQuery
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateOne(entity: string, id: PrimaryFieldType, input: EntityData) {
|
createOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||||
return this.patch<RepositoryResponse<EntityData>>([entity, id], input);
|
entity: E,
|
||||||
|
input: Omit<Data, "id">
|
||||||
|
) {
|
||||||
|
return this.post<RepositoryResponse<Data>>([entity as any], input);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteOne(entity: string, id: PrimaryFieldType) {
|
updateOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||||
return this.delete<RepositoryResponse<EntityData>>([entity, id]);
|
entity: E,
|
||||||
|
id: PrimaryFieldType,
|
||||||
|
input: Partial<Omit<Data, "id">>
|
||||||
|
) {
|
||||||
|
return this.patch<RepositoryResponse<Data>>([entity as any, id], input);
|
||||||
}
|
}
|
||||||
|
|
||||||
count(entity: string, where: RepoQuery["where"] = {}) {
|
deleteOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||||
return this.post<RepositoryResponse<{ entity: string; count: number }>>(
|
entity: E,
|
||||||
[entity, "fn", "count"],
|
id: PrimaryFieldType
|
||||||
|
) {
|
||||||
|
return this.delete<RepositoryResponse<Data>>([entity as any, id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
count<E extends keyof DB | string>(entity: E, where: RepoQuery["where"] = {}) {
|
||||||
|
return this.post<RepositoryResponse<{ entity: E; count: number }>>(
|
||||||
|
[entity as any, "fn", "count"],
|
||||||
where
|
where
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { type ClassController, isDebug, tbValidator as tb } from "core";
|
import { type ClassController, isDebug, tbValidator as tb } from "core";
|
||||||
import { Type, objectCleanEmpty, objectTransform } from "core/utils";
|
import { StringEnum, Type, objectCleanEmpty, objectTransform } from "core/utils";
|
||||||
import {
|
import {
|
||||||
DataPermissions,
|
DataPermissions,
|
||||||
type EntityData,
|
type EntityData,
|
||||||
@@ -165,13 +165,12 @@ export class DataController implements ClassController {
|
|||||||
// read entity schema
|
// read entity schema
|
||||||
.get("/schema.json", async (c) => {
|
.get("/schema.json", async (c) => {
|
||||||
this.guard.throwUnlessGranted(DataPermissions.entityRead);
|
this.guard.throwUnlessGranted(DataPermissions.entityRead);
|
||||||
const url = new URL(c.req.url);
|
const $id = `${this.config.basepath}/schema.json`;
|
||||||
const $id = `${url.origin}${this.config.basepath}/schema.json`;
|
|
||||||
const schemas = Object.fromEntries(
|
const schemas = Object.fromEntries(
|
||||||
this.em.entities.map((e) => [
|
this.em.entities.map((e) => [
|
||||||
e.name,
|
e.name,
|
||||||
{
|
{
|
||||||
$ref: `schemas/${e.name}`
|
$ref: `${this.config.basepath}/schemas/${e.name}`
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
@@ -183,22 +182,28 @@ export class DataController implements ClassController {
|
|||||||
})
|
})
|
||||||
// read schema
|
// read schema
|
||||||
.get(
|
.get(
|
||||||
"/schemas/:entity",
|
"/schemas/:entity/:context?",
|
||||||
tb("param", Type.Object({ entity: Type.String() })),
|
tb(
|
||||||
|
"param",
|
||||||
|
Type.Object({
|
||||||
|
entity: Type.String(),
|
||||||
|
context: Type.Optional(StringEnum(["create", "update"]))
|
||||||
|
})
|
||||||
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
this.guard.throwUnlessGranted(DataPermissions.entityRead);
|
this.guard.throwUnlessGranted(DataPermissions.entityRead);
|
||||||
|
|
||||||
//console.log("request", c.req.raw);
|
//console.log("request", c.req.raw);
|
||||||
const { entity } = c.req.param();
|
const { entity, context } = c.req.param();
|
||||||
if (!this.entityExists(entity)) {
|
if (!this.entityExists(entity)) {
|
||||||
console.log("not found", entity, definedEntities);
|
console.log("not found", entity, definedEntities);
|
||||||
return c.notFound();
|
return c.notFound();
|
||||||
}
|
}
|
||||||
const _entity = this.em.entity(entity);
|
const _entity = this.em.entity(entity);
|
||||||
const schema = _entity.toSchema();
|
const schema = _entity.toSchema({ context } as any);
|
||||||
const url = new URL(c.req.url);
|
const url = new URL(c.req.url);
|
||||||
const base = `${url.origin}${this.config.basepath}`;
|
const base = `${url.origin}${this.config.basepath}`;
|
||||||
const $id = `${base}/schemas/${entity}`;
|
const $id = `${this.config.basepath}/schemas/${entity}`;
|
||||||
return c.json({
|
return c.json({
|
||||||
$schema: `${base}/schema.json`,
|
$schema: `${base}/schema.json`,
|
||||||
$id,
|
$id,
|
||||||
|
|||||||
7
app/src/data/connection/DummyConnection.ts
Normal file
7
app/src/data/connection/DummyConnection.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Connection } from "./Connection";
|
||||||
|
|
||||||
|
export class DummyConnection extends Connection {
|
||||||
|
constructor() {
|
||||||
|
super(undefined as any);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -158,7 +158,7 @@ export class Entity<
|
|||||||
}
|
}
|
||||||
|
|
||||||
get label(): string {
|
get label(): string {
|
||||||
return snakeToPascalWithSpaces(this.config.name || this.name);
|
return this.config.name ?? snakeToPascalWithSpaces(this.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
field(name: string): Field | undefined {
|
field(name: string): Field | undefined {
|
||||||
@@ -210,20 +210,34 @@ export class Entity<
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
toSchema(clean?: boolean): object {
|
toSchema(options?: { clean: boolean; context?: "create" | "update" }): object {
|
||||||
const fields = Object.fromEntries(this.fields.map((field) => [field.name, field]));
|
let fields: Field[];
|
||||||
|
switch (options?.context) {
|
||||||
|
case "create":
|
||||||
|
case "update":
|
||||||
|
fields = this.getFillableFields(options.context);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
fields = this.getFields(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const _fields = Object.fromEntries(fields.map((field) => [field.name, field]));
|
||||||
const schema = Type.Object(
|
const schema = Type.Object(
|
||||||
transformObject(fields, (field) => ({
|
transformObject(_fields, (field) => {
|
||||||
|
//const hidden = field.isHidden(options?.context);
|
||||||
|
const fillable = field.isFillable(options?.context);
|
||||||
|
return {
|
||||||
title: field.config.label,
|
title: field.config.label,
|
||||||
$comment: field.config.description,
|
$comment: field.config.description,
|
||||||
$field: field.type,
|
$field: field.type,
|
||||||
readOnly: !field.isFillable("update") ? true : undefined,
|
readOnly: !fillable ? true : undefined,
|
||||||
writeOnly: !field.isFillable("create") ? true : undefined,
|
|
||||||
...field.toJsonSchema()
|
...field.toJsonSchema()
|
||||||
}))
|
};
|
||||||
|
}),
|
||||||
|
{ additionalProperties: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
return clean ? JSON.parse(JSON.stringify(schema)) : schema;
|
return options?.clean ? JSON.parse(JSON.stringify(schema)) : schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { DB as DefaultDB } from "core";
|
||||||
import { EventManager } from "core/events";
|
import { EventManager } from "core/events";
|
||||||
import { sql } from "kysely";
|
import { sql } from "kysely";
|
||||||
import { Connection } from "../connection/Connection";
|
import { Connection } from "../connection/Connection";
|
||||||
@@ -14,7 +15,18 @@ import { SchemaManager } from "../schema/SchemaManager";
|
|||||||
import { Entity } from "./Entity";
|
import { Entity } from "./Entity";
|
||||||
import { type EntityData, Mutator, Repository } from "./index";
|
import { type EntityData, Mutator, Repository } from "./index";
|
||||||
|
|
||||||
export class EntityManager<DB> {
|
type EntitySchema<
|
||||||
|
TBD extends object = DefaultDB,
|
||||||
|
E extends Entity | keyof TBD | string = string
|
||||||
|
> = E extends Entity<infer Name>
|
||||||
|
? Name extends keyof TBD
|
||||||
|
? Name
|
||||||
|
: never
|
||||||
|
: E extends keyof TBD
|
||||||
|
? E
|
||||||
|
: never;
|
||||||
|
|
||||||
|
export class EntityManager<TBD extends object = DefaultDB> {
|
||||||
connection: Connection;
|
connection: Connection;
|
||||||
|
|
||||||
private _entities: Entity[] = [];
|
private _entities: Entity[] = [];
|
||||||
@@ -50,7 +62,7 @@ export class EntityManager<DB> {
|
|||||||
* Forks the EntityManager without the EventManager.
|
* Forks the EntityManager without the EventManager.
|
||||||
* This is useful when used inside an event handler.
|
* This is useful when used inside an event handler.
|
||||||
*/
|
*/
|
||||||
fork(): EntityManager<DB> {
|
fork(): EntityManager {
|
||||||
return new EntityManager(this._entities, this.connection, this._relations, this._indices);
|
return new EntityManager(this._entities, this.connection, this._relations, this._indices);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,10 +99,17 @@ export class EntityManager<DB> {
|
|||||||
this.entities.push(entity);
|
this.entities.push(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
entity(name: string): Entity {
|
entity(e: Entity | keyof TBD | string): Entity {
|
||||||
const entity = this.entities.find((e) => e.name === name);
|
let entity: Entity | undefined;
|
||||||
|
if (typeof e === "string") {
|
||||||
|
entity = this.entities.find((entity) => entity.name === e);
|
||||||
|
} else if (e instanceof Entity) {
|
||||||
|
entity = e;
|
||||||
|
}
|
||||||
|
|
||||||
if (!entity) {
|
if (!entity) {
|
||||||
throw new EntityNotDefinedException(name);
|
// @ts-ignore
|
||||||
|
throw new EntityNotDefinedException(e instanceof Entity ? e.name : e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return entity;
|
return entity;
|
||||||
@@ -162,28 +181,18 @@ export class EntityManager<DB> {
|
|||||||
return this.relations.relationReferencesOf(this.entity(entity_name));
|
return this.relations.relationReferencesOf(this.entity(entity_name));
|
||||||
}
|
}
|
||||||
|
|
||||||
repository(_entity: Entity | string) {
|
repository<E extends Entity | keyof TBD | string>(
|
||||||
const entity = _entity instanceof Entity ? _entity : this.entity(_entity);
|
entity: E
|
||||||
return new Repository(this, entity, this.emgr);
|
): Repository<TBD, EntitySchema<TBD, E>> {
|
||||||
|
return this.repo(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
repo<E extends Entity>(
|
repo<E extends Entity | keyof TBD | string>(entity: E): Repository<TBD, EntitySchema<TBD, E>> {
|
||||||
_entity: E
|
return new Repository(this, this.entity(entity), this.emgr);
|
||||||
): Repository<
|
|
||||||
DB,
|
|
||||||
E extends Entity<infer Name> ? (Name extends keyof DB ? Name : never) : never
|
|
||||||
> {
|
|
||||||
return new Repository(this, _entity, this.emgr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_repo<TB extends keyof DB>(_entity: TB): Repository<DB, TB> {
|
mutator<E extends Entity | keyof TBD | string>(entity: E): Mutator<TBD, EntitySchema<TBD, E>> {
|
||||||
const entity = this.entity(_entity as any);
|
return new Mutator(this, this.entity(entity), this.emgr);
|
||||||
return new Repository(this, entity, this.emgr);
|
|
||||||
}
|
|
||||||
|
|
||||||
mutator(_entity: Entity | string) {
|
|
||||||
const entity = _entity instanceof Entity ? _entity : this.entity(_entity);
|
|
||||||
return new Mutator(this, entity, this.emgr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addIndex(index: EntityIndex, force = false) {
|
addIndex(index: EntityIndex, force = false) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { PrimaryFieldType } from "core";
|
import type { DB as DefaultDB, PrimaryFieldType } from "core";
|
||||||
import { type EmitsEvents, EventManager } from "core/events";
|
import { type EmitsEvents, EventManager } from "core/events";
|
||||||
import type { DeleteQueryBuilder, InsertQueryBuilder, UpdateQueryBuilder } from "kysely";
|
import type { DeleteQueryBuilder, InsertQueryBuilder, UpdateQueryBuilder } from "kysely";
|
||||||
import { type TActionContext, WhereBuilder } from "..";
|
import { type TActionContext, WhereBuilder } from "..";
|
||||||
@@ -25,8 +25,14 @@ export type MutatorResponse<T = EntityData[]> = {
|
|||||||
data: T;
|
data: T;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Mutator<DB> implements EmitsEvents {
|
export class Mutator<
|
||||||
em: EntityManager<DB>;
|
TBD extends object = DefaultDB,
|
||||||
|
TB extends keyof TBD = any,
|
||||||
|
Output = TBD[TB],
|
||||||
|
Input = Omit<Output, "id">
|
||||||
|
> implements EmitsEvents
|
||||||
|
{
|
||||||
|
em: EntityManager<TBD>;
|
||||||
entity: Entity;
|
entity: Entity;
|
||||||
static readonly Events = MutatorEvents;
|
static readonly Events = MutatorEvents;
|
||||||
emgr: EventManager<typeof MutatorEvents>;
|
emgr: EventManager<typeof MutatorEvents>;
|
||||||
@@ -37,7 +43,7 @@ export class Mutator<DB> implements EmitsEvents {
|
|||||||
this.__unstable_disable_system_entity_creation = value;
|
this.__unstable_disable_system_entity_creation = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(em: EntityManager<DB>, entity: Entity, emgr?: EventManager<any>) {
|
constructor(em: EntityManager<TBD>, entity: Entity, emgr?: EventManager<any>) {
|
||||||
this.em = em;
|
this.em = em;
|
||||||
this.entity = entity;
|
this.entity = entity;
|
||||||
this.emgr = emgr ?? new EventManager(MutatorEvents);
|
this.emgr = emgr ?? new EventManager(MutatorEvents);
|
||||||
@@ -47,13 +53,13 @@ export class Mutator<DB> implements EmitsEvents {
|
|||||||
return this.em.connection.kysely;
|
return this.em.connection.kysely;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getValidatedData(data: EntityData, context: TActionContext): Promise<EntityData> {
|
async getValidatedData<Given = any>(data: Given, context: TActionContext): Promise<Given> {
|
||||||
const entity = this.entity;
|
const entity = this.entity;
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("Context must be provided for validation");
|
throw new Error("Context must be provided for validation");
|
||||||
}
|
}
|
||||||
|
|
||||||
const keys = Object.keys(data);
|
const keys = Object.keys(data as any);
|
||||||
const validatedData: EntityData = {};
|
const validatedData: EntityData = {};
|
||||||
|
|
||||||
// get relational references/keys
|
// get relational references/keys
|
||||||
@@ -95,7 +101,7 @@ export class Mutator<DB> implements EmitsEvents {
|
|||||||
throw new Error(`No data left to update "${entity.name}"`);
|
throw new Error(`No data left to update "${entity.name}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return validatedData;
|
return validatedData as Given;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async many(qb: MutatorQB): Promise<MutatorResponse> {
|
protected async many(qb: MutatorQB): Promise<MutatorResponse> {
|
||||||
@@ -120,7 +126,7 @@ export class Mutator<DB> implements EmitsEvents {
|
|||||||
return { ...response, data: data[0]! };
|
return { ...response, data: data[0]! };
|
||||||
}
|
}
|
||||||
|
|
||||||
async insertOne(data: EntityData): Promise<MutatorResponse<EntityData>> {
|
async insertOne(data: Input): Promise<MutatorResponse<Output>> {
|
||||||
const entity = this.entity;
|
const entity = this.entity;
|
||||||
if (entity.type === "system" && this.__unstable_disable_system_entity_creation) {
|
if (entity.type === "system" && this.__unstable_disable_system_entity_creation) {
|
||||||
throw new Error(`Creation of system entity "${entity.name}" is disabled`);
|
throw new Error(`Creation of system entity "${entity.name}" is disabled`);
|
||||||
@@ -154,10 +160,10 @@ export class Mutator<DB> implements EmitsEvents {
|
|||||||
|
|
||||||
await this.emgr.emit(new Mutator.Events.MutatorInsertAfter({ entity, data: res.data }));
|
await this.emgr.emit(new Mutator.Events.MutatorInsertAfter({ entity, data: res.data }));
|
||||||
|
|
||||||
return res;
|
return res as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateOne(id: PrimaryFieldType, data: EntityData): Promise<MutatorResponse<EntityData>> {
|
async updateOne(id: PrimaryFieldType, data: Partial<Input>): Promise<MutatorResponse<Output>> {
|
||||||
const entity = this.entity;
|
const entity = this.entity;
|
||||||
if (!Number.isInteger(id)) {
|
if (!Number.isInteger(id)) {
|
||||||
throw new Error("ID must be provided for update");
|
throw new Error("ID must be provided for update");
|
||||||
@@ -166,12 +172,16 @@ export class Mutator<DB> implements EmitsEvents {
|
|||||||
const validatedData = await this.getValidatedData(data, "update");
|
const validatedData = await this.getValidatedData(data, "update");
|
||||||
|
|
||||||
await this.emgr.emit(
|
await this.emgr.emit(
|
||||||
new Mutator.Events.MutatorUpdateBefore({ entity, entityId: id, data: validatedData })
|
new Mutator.Events.MutatorUpdateBefore({
|
||||||
|
entity,
|
||||||
|
entityId: id,
|
||||||
|
data: validatedData as any
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const query = this.conn
|
const query = this.conn
|
||||||
.updateTable(entity.name)
|
.updateTable(entity.name)
|
||||||
.set(validatedData)
|
.set(validatedData as any)
|
||||||
.where(entity.id().name, "=", id)
|
.where(entity.id().name, "=", id)
|
||||||
.returning(entity.getSelect());
|
.returning(entity.getSelect());
|
||||||
|
|
||||||
@@ -181,10 +191,10 @@ export class Mutator<DB> implements EmitsEvents {
|
|||||||
new Mutator.Events.MutatorUpdateAfter({ entity, entityId: id, data: res.data })
|
new Mutator.Events.MutatorUpdateAfter({ entity, entityId: id, data: res.data })
|
||||||
);
|
);
|
||||||
|
|
||||||
return res;
|
return res as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteOne(id: PrimaryFieldType): Promise<MutatorResponse<EntityData>> {
|
async deleteOne(id: PrimaryFieldType): Promise<MutatorResponse<Output>> {
|
||||||
const entity = this.entity;
|
const entity = this.entity;
|
||||||
if (!Number.isInteger(id)) {
|
if (!Number.isInteger(id)) {
|
||||||
throw new Error("ID must be provided for deletion");
|
throw new Error("ID must be provided for deletion");
|
||||||
@@ -203,7 +213,7 @@ export class Mutator<DB> implements EmitsEvents {
|
|||||||
new Mutator.Events.MutatorDeleteAfter({ entity, entityId: id, data: res.data })
|
new Mutator.Events.MutatorDeleteAfter({ entity, entityId: id, data: res.data })
|
||||||
);
|
);
|
||||||
|
|
||||||
return res;
|
return res as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getValidOptions(options?: Partial<RepoQuery>): Partial<RepoQuery> {
|
private getValidOptions(options?: Partial<RepoQuery>): Partial<RepoQuery> {
|
||||||
@@ -250,47 +260,62 @@ export class Mutator<DB> implements EmitsEvents {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @todo: decide whether entries should be deleted all at once or one by one (for events)
|
// @todo: decide whether entries should be deleted all at once or one by one (for events)
|
||||||
async deleteWhere(where?: RepoQuery["where"]): Promise<MutatorResponse<EntityData>> {
|
async deleteWhere(where?: RepoQuery["where"]): Promise<MutatorResponse<Output[]>> {
|
||||||
const entity = this.entity;
|
const entity = this.entity;
|
||||||
|
|
||||||
const qb = this.appendWhere(this.conn.deleteFrom(entity.name), where).returning(
|
const qb = this.appendWhere(this.conn.deleteFrom(entity.name), where).returning(
|
||||||
entity.getSelect()
|
entity.getSelect()
|
||||||
);
|
);
|
||||||
|
|
||||||
//await this.emgr.emit(new Mutator.Events.MutatorDeleteBefore({ entity, entityId: id }));
|
return (await this.many(qb)) as any;
|
||||||
|
|
||||||
const res = await this.many(qb);
|
|
||||||
|
|
||||||
/*await this.emgr.emit(
|
|
||||||
new Mutator.Events.MutatorDeleteAfter({ entity, entityId: id, data: res.data })
|
|
||||||
);*/
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateWhere(
|
async updateWhere(
|
||||||
data: EntityData,
|
data: Partial<Input>,
|
||||||
where?: RepoQuery["where"]
|
where?: RepoQuery["where"]
|
||||||
): Promise<MutatorResponse<EntityData>> {
|
): Promise<MutatorResponse<Output[]>> {
|
||||||
const entity = this.entity;
|
const entity = this.entity;
|
||||||
|
|
||||||
const validatedData = await this.getValidatedData(data, "update");
|
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)
|
const query = this.appendWhere(this.conn.updateTable(entity.name), where)
|
||||||
.set(validatedData)
|
.set(validatedData as any)
|
||||||
//.where(entity.id().name, "=", id)
|
|
||||||
.returning(entity.getSelect());
|
.returning(entity.getSelect());
|
||||||
|
|
||||||
const res = await this.many(query);
|
return (await this.many(query)) as any;
|
||||||
|
}
|
||||||
|
|
||||||
/*await this.emgr.emit(
|
async insertMany(data: Input[]): Promise<MutatorResponse<Output[]>> {
|
||||||
new Mutator.Events.MutatorUpdateAfter({ entity, entityId: id, data: res.data })
|
const entity = this.entity;
|
||||||
);*/
|
if (entity.type === "system" && this.__unstable_disable_system_entity_creation) {
|
||||||
|
throw new Error(`Creation of system entity "${entity.name}" is disabled`);
|
||||||
|
}
|
||||||
|
|
||||||
return res;
|
const validated: any[] = [];
|
||||||
|
for (const row of data) {
|
||||||
|
const validatedData = {
|
||||||
|
...entity.getDefaultObject(),
|
||||||
|
...(await this.getValidatedData(row, "create"))
|
||||||
|
};
|
||||||
|
|
||||||
|
// check if required fields are present
|
||||||
|
const required = entity.getRequiredFields();
|
||||||
|
for (const field of required) {
|
||||||
|
if (
|
||||||
|
typeof validatedData[field.name] === "undefined" ||
|
||||||
|
validatedData[field.name] === null
|
||||||
|
) {
|
||||||
|
throw new Error(`Field "${field.name}" is required`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validated.push(validatedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = this.conn
|
||||||
|
.insertInto(entity.name)
|
||||||
|
.values(validated)
|
||||||
|
.returning(entity.getSelect());
|
||||||
|
|
||||||
|
return (await this.many(query)) as any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { PrimaryFieldType } from "core";
|
import type { DB as DefaultDB, PrimaryFieldType } from "core";
|
||||||
import { type EmitsEvents, EventManager } from "core/events";
|
import { type EmitsEvents, EventManager } from "core/events";
|
||||||
import { type SelectQueryBuilder, sql } from "kysely";
|
import { type SelectQueryBuilder, sql } from "kysely";
|
||||||
import { cloneDeep } from "lodash-es";
|
import { cloneDeep } from "lodash-es";
|
||||||
@@ -43,13 +43,15 @@ export type RepositoryExistsResponse = RepositoryRawResponse & {
|
|||||||
exists: boolean;
|
exists: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Repository<DB = any, TB extends keyof DB = any> implements EmitsEvents {
|
export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = any>
|
||||||
em: EntityManager<DB>;
|
implements EmitsEvents
|
||||||
|
{
|
||||||
|
em: EntityManager<TBD>;
|
||||||
entity: Entity;
|
entity: Entity;
|
||||||
static readonly Events = RepositoryEvents;
|
static readonly Events = RepositoryEvents;
|
||||||
emgr: EventManager<typeof Repository.Events>;
|
emgr: EventManager<typeof Repository.Events>;
|
||||||
|
|
||||||
constructor(em: EntityManager<DB>, entity: Entity, emgr?: EventManager<any>) {
|
constructor(em: EntityManager<TBD>, entity: Entity, emgr?: EventManager<any>) {
|
||||||
this.em = em;
|
this.em = em;
|
||||||
this.entity = entity;
|
this.entity = entity;
|
||||||
this.emgr = emgr ?? new EventManager(MutatorEvents);
|
this.emgr = emgr ?? new EventManager(MutatorEvents);
|
||||||
@@ -272,7 +274,7 @@ export class Repository<DB = any, TB extends keyof DB = any> implements EmitsEve
|
|||||||
async findId(
|
async findId(
|
||||||
id: PrimaryFieldType,
|
id: PrimaryFieldType,
|
||||||
_options?: Partial<Omit<RepoQuery, "where" | "limit" | "offset">>
|
_options?: Partial<Omit<RepoQuery, "where" | "limit" | "offset">>
|
||||||
): Promise<RepositoryResponse<DB[TB]>> {
|
): Promise<RepositoryResponse<TBD[TB] | undefined>> {
|
||||||
const { qb, options } = this.buildQuery(
|
const { qb, options } = this.buildQuery(
|
||||||
{
|
{
|
||||||
..._options,
|
..._options,
|
||||||
@@ -288,7 +290,7 @@ export class Repository<DB = any, TB extends keyof DB = any> implements EmitsEve
|
|||||||
async findOne(
|
async findOne(
|
||||||
where: RepoQuery["where"],
|
where: RepoQuery["where"],
|
||||||
_options?: Partial<Omit<RepoQuery, "where" | "limit" | "offset">>
|
_options?: Partial<Omit<RepoQuery, "where" | "limit" | "offset">>
|
||||||
): Promise<RepositoryResponse<DB[TB] | undefined>> {
|
): Promise<RepositoryResponse<TBD[TB] | undefined>> {
|
||||||
const { qb, options } = this.buildQuery({
|
const { qb, options } = this.buildQuery({
|
||||||
..._options,
|
..._options,
|
||||||
where,
|
where,
|
||||||
@@ -298,7 +300,7 @@ export class Repository<DB = any, TB extends keyof DB = any> implements EmitsEve
|
|||||||
return this.single(qb, options) as any;
|
return this.single(qb, options) as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findMany(_options?: Partial<RepoQuery>): Promise<RepositoryResponse<DB[TB][]>> {
|
async findMany(_options?: Partial<RepoQuery>): Promise<RepositoryResponse<TBD[TB][]>> {
|
||||||
const { qb, options } = this.buildQuery(_options);
|
const { qb, options } = this.buildQuery(_options);
|
||||||
//console.log("findMany:options", options);
|
//console.log("findMany:options", options);
|
||||||
|
|
||||||
|
|||||||
@@ -104,6 +104,12 @@ export class TextField<Required extends true | false = false> extends Field<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.config.pattern && value && !new RegExp(this.config.pattern).test(value)) {
|
||||||
|
throw new TransformPersistFailedException(
|
||||||
|
`Field "${this.name}" must match the pattern ${this.config.pattern}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ export { LibsqlConnection, type LibSqlCredentials } from "./connection/LibsqlCon
|
|||||||
export { SqliteConnection } from "./connection/SqliteConnection";
|
export { SqliteConnection } from "./connection/SqliteConnection";
|
||||||
export { SqliteLocalConnection } from "./connection/SqliteLocalConnection";
|
export { SqliteLocalConnection } from "./connection/SqliteLocalConnection";
|
||||||
|
|
||||||
|
export { constructEntity, constructRelation } from "./schema/constructor";
|
||||||
|
|
||||||
export const DatabaseEvents = {
|
export const DatabaseEvents = {
|
||||||
...MutatorEvents,
|
...MutatorEvents,
|
||||||
...RepositoryEvents
|
...RepositoryEvents
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
import { DummyConnection } from "data/connection/DummyConnection";
|
||||||
|
import { EntityManager } from "data/entities/EntityManager";
|
||||||
|
import type { Generated } from "kysely";
|
||||||
|
import { MediaField, type MediaFieldConfig, type MediaItem } from "media/MediaField";
|
||||||
|
import type { ModuleConfigs } from "modules";
|
||||||
import {
|
import {
|
||||||
BooleanField,
|
BooleanField,
|
||||||
type BooleanFieldConfig,
|
type BooleanFieldConfig,
|
||||||
@@ -5,6 +10,8 @@ import {
|
|||||||
type DateFieldConfig,
|
type DateFieldConfig,
|
||||||
Entity,
|
Entity,
|
||||||
type EntityConfig,
|
type EntityConfig,
|
||||||
|
EntityIndex,
|
||||||
|
type EntityRelation,
|
||||||
EnumField,
|
EnumField,
|
||||||
type EnumFieldConfig,
|
type EnumFieldConfig,
|
||||||
type Field,
|
type Field,
|
||||||
@@ -25,15 +32,14 @@ import {
|
|||||||
type TEntityType,
|
type TEntityType,
|
||||||
TextField,
|
TextField,
|
||||||
type TextFieldConfig
|
type TextFieldConfig
|
||||||
} from "data";
|
} from "../index";
|
||||||
import type { Generated } from "kysely";
|
|
||||||
import { MediaField, type MediaFieldConfig, type MediaItem } from "media/MediaField";
|
|
||||||
|
|
||||||
type Options<Config = any> = {
|
type Options<Config = any> = {
|
||||||
entity: { name: string; fields: Record<string, Field<any, any, any>> };
|
entity: { name: string; fields: Record<string, Field<any, any, any>> };
|
||||||
field_name: string;
|
field_name: string;
|
||||||
config: Config;
|
config: Config;
|
||||||
is_required: boolean;
|
is_required: boolean;
|
||||||
|
another?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FieldMap = {
|
const FieldMap = {
|
||||||
@@ -239,7 +245,89 @@ export function relation<Local extends Entity>(local: Local) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
type InferEntityFields<T> = T extends Entity<infer _N, infer Fields>
|
export function index<E extends Entity>(entity: E) {
|
||||||
|
return {
|
||||||
|
on: (fields: (keyof InsertSchema<E>)[], unique?: boolean) => {
|
||||||
|
const _fields = fields.map((f) => {
|
||||||
|
const field = entity.field(f as any);
|
||||||
|
if (!field) {
|
||||||
|
throw new Error(`Field "${String(f)}" not found on entity "${entity.name}"`);
|
||||||
|
}
|
||||||
|
return field;
|
||||||
|
});
|
||||||
|
return new EntityIndex(entity, _fields, unique);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class EntityManagerPrototype<Entities extends Record<string, Entity>> extends EntityManager<
|
||||||
|
Schema<Entities>
|
||||||
|
> {
|
||||||
|
constructor(
|
||||||
|
public __entities: Entities,
|
||||||
|
relations: EntityRelation[] = [],
|
||||||
|
indices: EntityIndex[] = []
|
||||||
|
) {
|
||||||
|
super(Object.values(__entities), new DummyConnection(), relations, indices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Chained<Fn extends (...args: any[]) => any, Rt = ReturnType<Fn>> = <E extends Entity>(
|
||||||
|
e: E
|
||||||
|
) => {
|
||||||
|
[K in keyof Rt]: Rt[K] extends (...args: any[]) => any
|
||||||
|
? (...args: Parameters<Rt[K]>) => Rt
|
||||||
|
: never;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function em<Entities extends Record<string, Entity>>(
|
||||||
|
entities: Entities,
|
||||||
|
schema?: (
|
||||||
|
fns: { relation: Chained<typeof relation>; index: Chained<typeof index> },
|
||||||
|
entities: Entities
|
||||||
|
) => void
|
||||||
|
) {
|
||||||
|
const relations: EntityRelation[] = [];
|
||||||
|
const indices: EntityIndex[] = [];
|
||||||
|
|
||||||
|
const relationProxy = (e: Entity) => {
|
||||||
|
return new Proxy(relation(e), {
|
||||||
|
get(target, prop) {
|
||||||
|
return (...args: any[]) => {
|
||||||
|
relations.push(target[prop](...args));
|
||||||
|
return relationProxy(e);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}) as any;
|
||||||
|
};
|
||||||
|
|
||||||
|
const indexProxy = (e: Entity) => {
|
||||||
|
return new Proxy(index(e), {
|
||||||
|
get(target, prop) {
|
||||||
|
return (...args: any[]) => {
|
||||||
|
indices.push(target[prop](...args));
|
||||||
|
return indexProxy(e);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}) as any;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (schema) {
|
||||||
|
schema({ relation: relationProxy, index: indexProxy }, entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
const e = new EntityManagerPrototype(entities, relations, indices);
|
||||||
|
return {
|
||||||
|
DB: e.__entities as unknown as Schemas<Entities>,
|
||||||
|
entities: e.__entities,
|
||||||
|
relations,
|
||||||
|
indices,
|
||||||
|
toJSON: () =>
|
||||||
|
e.toJSON() as unknown as Pick<ModuleConfigs["data"], "entities" | "relations" | "indices">
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InferEntityFields<T> = T extends Entity<infer _N, infer Fields>
|
||||||
? {
|
? {
|
||||||
[K in keyof Fields]: Fields[K] extends { _type: infer Type; _required: infer Required }
|
[K in keyof Fields]: Fields[K] extends { _type: infer Type; _required: infer Required }
|
||||||
? Required extends true
|
? Required extends true
|
||||||
@@ -284,12 +372,16 @@ type OptionalUndefined<
|
|||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
type InferField<Field> = Field extends { _type: infer Type; _required: infer Required }
|
export type InferField<Field> = Field extends { _type: infer Type; _required: infer Required }
|
||||||
? Required extends true
|
? Required extends true
|
||||||
? Type
|
? Type
|
||||||
: Type | undefined
|
: Type | undefined
|
||||||
: never;
|
: never;
|
||||||
|
|
||||||
|
export type Schemas<T extends Record<string, Entity>> = {
|
||||||
|
[K in keyof T]: Schema<T[K]>;
|
||||||
|
};
|
||||||
|
|
||||||
export type InsertSchema<T> = Simplify<OptionalUndefined<InferEntityFields<T>>>;
|
export type InsertSchema<T> = Simplify<OptionalUndefined<InferEntityFields<T>>>;
|
||||||
export type Schema<T> = { id: Generated<number> } & InsertSchema<T>;
|
export type Schema<T> = Simplify<{ id: Generated<number> } & InsertSchema<T>>;
|
||||||
export type FieldSchema<T> = Simplify<OptionalUndefined<InferFields<T>>>;
|
export type FieldSchema<T> = Simplify<OptionalUndefined<InferFields<T>>>;
|
||||||
|
|||||||
34
app/src/data/schema/constructor.ts
Normal file
34
app/src/data/schema/constructor.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { transformObject } from "core/utils";
|
||||||
|
import { Entity, type Field } from "data";
|
||||||
|
import { FIELDS, RELATIONS, type TAppDataEntity, type TAppDataRelation } from "data/data-schema";
|
||||||
|
|
||||||
|
export function constructEntity(name: string, entityConfig: TAppDataEntity) {
|
||||||
|
const fields = transformObject(entityConfig.fields ?? {}, (fieldConfig, name) => {
|
||||||
|
const { type } = fieldConfig;
|
||||||
|
if (!(type in FIELDS)) {
|
||||||
|
throw new Error(`Field type "${type}" not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { field } = FIELDS[type as any];
|
||||||
|
const returnal = new field(name, fieldConfig.config) as Field;
|
||||||
|
return returnal;
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Entity(
|
||||||
|
name,
|
||||||
|
Object.values(fields),
|
||||||
|
entityConfig.config as any,
|
||||||
|
entityConfig.type as any
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function constructRelation(
|
||||||
|
relationConfig: TAppDataRelation,
|
||||||
|
resolver: (name: Entity | string) => Entity
|
||||||
|
) {
|
||||||
|
return new RELATIONS[relationConfig.type].cls(
|
||||||
|
resolver(relationConfig.source),
|
||||||
|
resolver(relationConfig.target),
|
||||||
|
relationConfig.config
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,8 +4,12 @@ export {
|
|||||||
getDefaultConfig,
|
getDefaultConfig,
|
||||||
getDefaultSchema,
|
getDefaultSchema,
|
||||||
type ModuleConfigs,
|
type ModuleConfigs,
|
||||||
type ModuleSchemas
|
type ModuleSchemas,
|
||||||
} from "modules/ModuleManager";
|
type ModuleManagerOptions,
|
||||||
|
type ModuleBuildContext
|
||||||
|
} from "./modules/ModuleManager";
|
||||||
|
|
||||||
|
export { registries } from "modules/registries";
|
||||||
|
|
||||||
export type * from "./adapter";
|
export type * from "./adapter";
|
||||||
export { Api, type ApiOptions } from "./Api";
|
export { Api, type ApiOptions } from "./Api";
|
||||||
|
|||||||
@@ -1,24 +1,15 @@
|
|||||||
|
import type { PrimaryFieldType } from "core";
|
||||||
import { EntityIndex, type EntityManager } from "data";
|
import { EntityIndex, type EntityManager } from "data";
|
||||||
import { type FileUploadedEventData, Storage, type StorageAdapter } from "media";
|
import { type FileUploadedEventData, Storage, type StorageAdapter } from "media";
|
||||||
import { Module } from "modules/Module";
|
import { Module } from "modules/Module";
|
||||||
import {
|
import { type FieldSchema, boolean, datetime, entity, json, number, text } from "../data/prototype";
|
||||||
type FieldSchema,
|
|
||||||
type InferFields,
|
|
||||||
type Schema,
|
|
||||||
boolean,
|
|
||||||
datetime,
|
|
||||||
entity,
|
|
||||||
json,
|
|
||||||
number,
|
|
||||||
text
|
|
||||||
} from "../data/prototype";
|
|
||||||
import { MediaController } from "./api/MediaController";
|
import { MediaController } from "./api/MediaController";
|
||||||
import { ADAPTERS, buildMediaSchema, type mediaConfigSchema, registry } from "./media-schema";
|
import { ADAPTERS, buildMediaSchema, type mediaConfigSchema, registry } from "./media-schema";
|
||||||
|
|
||||||
export type MediaFieldSchema = FieldSchema<typeof AppMedia.mediaFields>;
|
export type MediaFieldSchema = FieldSchema<typeof AppMedia.mediaFields>;
|
||||||
declare global {
|
declare module "core" {
|
||||||
interface DB {
|
interface DB {
|
||||||
media: MediaFieldSchema;
|
media: { id: PrimaryFieldType } & MediaFieldSchema;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,14 +103,14 @@ export class AppMedia extends Module<typeof mediaConfigSchema> {
|
|||||||
return this.em.entity(entity_name);
|
return this.em.entity(entity_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
get em(): EntityManager<DB> {
|
get em(): EntityManager {
|
||||||
return this.ctx.em;
|
return this.ctx.em;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupListeners() {
|
private setupListeners() {
|
||||||
//const media = this._entity;
|
//const media = this._entity;
|
||||||
const { emgr, em } = this.ctx;
|
const { emgr, em } = this.ctx;
|
||||||
const media = this.getMediaEntity();
|
const media = this.getMediaEntity().name as "media";
|
||||||
|
|
||||||
// when file is uploaded, sync with media entity
|
// when file is uploaded, sync with media entity
|
||||||
// @todo: need a way for singleton events!
|
// @todo: need a way for singleton events!
|
||||||
@@ -140,10 +131,10 @@ export class AppMedia extends Module<typeof mediaConfigSchema> {
|
|||||||
Storage.Events.FileDeletedEvent,
|
Storage.Events.FileDeletedEvent,
|
||||||
async (e) => {
|
async (e) => {
|
||||||
// simple file deletion sync
|
// simple file deletion sync
|
||||||
const item = await em.repo(media).findOne({ path: e.params.name });
|
const { data } = await em.repo(media).findOne({ path: e.params.name });
|
||||||
if (item.data) {
|
if (data) {
|
||||||
console.log("item.data", item.data);
|
console.log("item.data", data);
|
||||||
await em.mutator(media).deleteOne(item.data.id);
|
await em.mutator(media).deleteOne(data.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("App:storage:file deleted", e);
|
console.log("App:storage:file deleted", e);
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ export class MediaController implements ClassController {
|
|||||||
const result = await mutator.insertOne({
|
const result = await mutator.insertOne({
|
||||||
...this.media.uploadedEventDataToMediaPayload(info),
|
...this.media.uploadedEventDataToMediaPayload(info),
|
||||||
...mediaRef
|
...mediaRef
|
||||||
});
|
} as any);
|
||||||
mutator.__unstable_toggleSystemEntityCreation(true);
|
mutator.__unstable_toggleSystemEntityCreation(true);
|
||||||
|
|
||||||
// delete items if needed
|
// delete items if needed
|
||||||
|
|||||||
@@ -17,10 +17,6 @@ import {
|
|||||||
import { type S3AdapterConfig, StorageS3Adapter } from "./storage/adapters/StorageS3Adapter";
|
import { type S3AdapterConfig, StorageS3Adapter } from "./storage/adapters/StorageS3Adapter";
|
||||||
|
|
||||||
export { StorageS3Adapter, type S3AdapterConfig, StorageCloudinaryAdapter, type CloudinaryConfig };
|
export { StorageS3Adapter, type S3AdapterConfig, StorageCloudinaryAdapter, type CloudinaryConfig };
|
||||||
/*export {
|
|
||||||
StorageLocalAdapter,
|
|
||||||
type LocalAdapterConfig
|
|
||||||
} from "./storage/adapters/StorageLocalAdapter";*/
|
|
||||||
|
|
||||||
export * as StorageEvents from "./storage/events";
|
export * as StorageEvents from "./storage/events";
|
||||||
export { type FileUploadedEventData } from "./storage/events";
|
export { type FileUploadedEventData } from "./storage/events";
|
||||||
@@ -31,16 +27,12 @@ type ClassThatImplements<T> = Constructor<T> & { prototype: T };
|
|||||||
export const MediaAdapterRegistry = new Registry<{
|
export const MediaAdapterRegistry = new Registry<{
|
||||||
cls: ClassThatImplements<StorageAdapter>;
|
cls: ClassThatImplements<StorageAdapter>;
|
||||||
schema: TObject;
|
schema: TObject;
|
||||||
}>().set({
|
}>((cls: ClassThatImplements<StorageAdapter>) => ({
|
||||||
s3: {
|
cls,
|
||||||
cls: StorageS3Adapter,
|
schema: cls.prototype.getSchema() as TObject
|
||||||
schema: StorageS3Adapter.prototype.getSchema()
|
}))
|
||||||
},
|
.register("s3", StorageS3Adapter)
|
||||||
cloudinary: {
|
.register("cloudinary", StorageCloudinaryAdapter);
|
||||||
cls: StorageCloudinaryAdapter,
|
|
||||||
schema: StorageCloudinaryAdapter.prototype.getSchema()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const Adapters = {
|
export const Adapters = {
|
||||||
s3: {
|
s3: {
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
import { readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
|
import { readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
|
||||||
import { type Static, Type, parse } from "core/utils";
|
import { type Static, Type, parse } from "core/utils";
|
||||||
import type {
|
import type { FileBody, FileListObject, FileMeta, StorageAdapter } from "../../Storage";
|
||||||
FileBody,
|
import { guess } from "../../mime-types-tiny";
|
||||||
FileListObject,
|
|
||||||
FileMeta,
|
|
||||||
FileUploadPayload,
|
|
||||||
StorageAdapter
|
|
||||||
} from "../../Storage";
|
|
||||||
import { guessMimeType } from "../../mime-types";
|
|
||||||
|
|
||||||
export const localAdapterConfig = Type.Object(
|
export const localAdapterConfig = Type.Object(
|
||||||
{
|
{
|
||||||
path: Type.String()
|
path: Type.String({ default: "./" })
|
||||||
},
|
},
|
||||||
{ title: "Local" }
|
{ title: "Local" }
|
||||||
);
|
);
|
||||||
@@ -89,7 +83,7 @@ export class StorageLocalAdapter implements StorageAdapter {
|
|||||||
async getObject(key: string, headers: Headers): Promise<Response> {
|
async getObject(key: string, headers: Headers): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const content = await readFile(`${this.config.path}/${key}`);
|
const content = await readFile(`${this.config.path}/${key}`);
|
||||||
const mimeType = guessMimeType(key);
|
const mimeType = guess(key);
|
||||||
|
|
||||||
return new Response(content, {
|
return new Response(content, {
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -111,7 +105,7 @@ export class StorageLocalAdapter implements StorageAdapter {
|
|||||||
async getObjectMeta(key: string): Promise<FileMeta> {
|
async getObjectMeta(key: string): Promise<FileMeta> {
|
||||||
const stats = await stat(`${this.config.path}/${key}`);
|
const stats = await stat(`${this.config.path}/${key}`);
|
||||||
return {
|
return {
|
||||||
type: guessMimeType(key) || "application/octet-stream",
|
type: guess(key) || "application/octet-stream",
|
||||||
size: stats.size
|
size: stats.size
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
77
app/src/media/storage/mime-types-tiny.ts
Normal file
77
app/src/media/storage/mime-types-tiny.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
export const Q = {
|
||||||
|
video: ["mp4", "webm"],
|
||||||
|
audio: ["ogg"],
|
||||||
|
image: ["jpeg", "png", "gif", "webp", "bmp", "tiff"],
|
||||||
|
text: ["html", "css", "mdx", "yaml", "vcard", "csv", "vtt"],
|
||||||
|
application: ["zip", "xml", "toml", "json", "json5"],
|
||||||
|
font: ["woff", "woff2", "ttf", "otf"]
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// reduced
|
||||||
|
const c = {
|
||||||
|
vnd: "vnd.openxmlformats-officedocument",
|
||||||
|
z: "application/x-7z-compressed",
|
||||||
|
t: (w = "plain") => `text/${w}`,
|
||||||
|
a: (w = "octet-stream") => `application/${w}`,
|
||||||
|
i: (w) => `image/${w}`,
|
||||||
|
v: (w) => `video/${w}`
|
||||||
|
} as const;
|
||||||
|
export const M = new Map<string, string>([
|
||||||
|
["7z", c.z],
|
||||||
|
["7zip", c.z],
|
||||||
|
["ai", c.a("pdf")],
|
||||||
|
["apk", c.a("vnd.android.package-archive")],
|
||||||
|
["doc", c.a("msword")],
|
||||||
|
["docx", `${c.vnd}.wordprocessingml.document`],
|
||||||
|
["eps", c.a("postscript")],
|
||||||
|
["epub", c.a("epub+zip")],
|
||||||
|
["ini", c.t()],
|
||||||
|
["jar", c.a("java-archive")],
|
||||||
|
["jsonld", c.a("ld+json")],
|
||||||
|
["jpg", c.i("jpeg")],
|
||||||
|
["log", c.t()],
|
||||||
|
["m3u", c.t()],
|
||||||
|
["m3u8", c.a("vnd.apple.mpegurl")],
|
||||||
|
["manifest", c.t("cache-manifest")],
|
||||||
|
["md", c.t("markdown")],
|
||||||
|
["mkv", c.v("x-matroska")],
|
||||||
|
["mp3", c.a("mpeg")],
|
||||||
|
["mobi", c.a("x-mobipocket-ebook")],
|
||||||
|
["ppt", c.a("powerpoint")],
|
||||||
|
["pptx", `${c.vnd}.presentationml.presentation`],
|
||||||
|
["qt", c.v("quicktime")],
|
||||||
|
["svg", c.i("svg+xml")],
|
||||||
|
["tif", c.i("tiff")],
|
||||||
|
["tsv", c.t("tab-separated-values")],
|
||||||
|
["tgz", c.a("x-tar")],
|
||||||
|
["txt", c.t()],
|
||||||
|
["text", c.t()],
|
||||||
|
["vcd", c.a("x-cdlink")],
|
||||||
|
["vcs", c.t("x-vcalendar")],
|
||||||
|
["wav", c.a("x-wav")],
|
||||||
|
["webmanifest", c.a("manifest+json")],
|
||||||
|
["xls", c.a("vnd.ms-excel")],
|
||||||
|
["xlsx", `${c.vnd}.spreadsheetml.sheet`],
|
||||||
|
["yml", c.t("yaml")]
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function guess(f: string): string {
|
||||||
|
try {
|
||||||
|
const e = f.split(".").pop() as string;
|
||||||
|
if (!e) {
|
||||||
|
return c.a();
|
||||||
|
}
|
||||||
|
|
||||||
|
// try quick first
|
||||||
|
for (const [t, _e] of Object.entries(Q)) {
|
||||||
|
// @ts-ignore
|
||||||
|
if (_e.includes(e)) {
|
||||||
|
return `${t}/${e}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return M.get(e!) as string;
|
||||||
|
} catch (e) {
|
||||||
|
return c.a();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import type { Hono } from "hono";
|
|||||||
export type ModuleBuildContext = {
|
export type ModuleBuildContext = {
|
||||||
connection: Connection;
|
connection: Connection;
|
||||||
server: Hono<any>;
|
server: Hono<any>;
|
||||||
em: EntityManager<any>;
|
em: EntityManager;
|
||||||
emgr: EventManager<any>;
|
emgr: EventManager<any>;
|
||||||
guard: Guard;
|
guard: Guard;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Guard } from "auth";
|
import { Guard } from "auth";
|
||||||
import { BkndError, DebugLogger, Exception, isDebug } from "core";
|
import { BkndError, DebugLogger } from "core";
|
||||||
import { EventManager } from "core/events";
|
import { EventManager } from "core/events";
|
||||||
import { clone, diff } from "core/object/diff";
|
import { clone, diff } from "core/object/diff";
|
||||||
import {
|
import {
|
||||||
@@ -35,9 +35,11 @@ import { AppFlows } from "../flows/AppFlows";
|
|||||||
import { AppMedia } from "../media/AppMedia";
|
import { AppMedia } from "../media/AppMedia";
|
||||||
import type { Module, ModuleBuildContext } from "./Module";
|
import type { Module, ModuleBuildContext } from "./Module";
|
||||||
|
|
||||||
|
export type { ModuleBuildContext };
|
||||||
|
|
||||||
export const MODULES = {
|
export const MODULES = {
|
||||||
server: AppServer,
|
server: AppServer,
|
||||||
data: AppData<any>,
|
data: AppData,
|
||||||
auth: AppAuth,
|
auth: AppAuth,
|
||||||
media: AppMedia,
|
media: AppMedia,
|
||||||
flows: AppFlows
|
flows: AppFlows
|
||||||
@@ -73,9 +75,14 @@ export type ModuleManagerOptions = {
|
|||||||
module: Module,
|
module: Module,
|
||||||
config: ModuleConfigs[Module]
|
config: ModuleConfigs[Module]
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
// triggered when no config table existed
|
||||||
|
onFirstBoot?: () => Promise<void>;
|
||||||
// base path for the hono instance
|
// base path for the hono instance
|
||||||
basePath?: string;
|
basePath?: string;
|
||||||
|
// doesn't perform validity checks for given/fetched config
|
||||||
trustFetched?: boolean;
|
trustFetched?: boolean;
|
||||||
|
// runs when initial config provided on a fresh database
|
||||||
|
seed?: (ctx: ModuleBuildContext) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ConfigTable<Json = ModuleConfigs> = {
|
type ConfigTable<Json = ModuleConfigs> = {
|
||||||
@@ -105,9 +112,9 @@ const __bknd = entity(TABLE_NAME, {
|
|||||||
updated_at: datetime()
|
updated_at: datetime()
|
||||||
});
|
});
|
||||||
type ConfigTable2 = Schema<typeof __bknd>;
|
type ConfigTable2 = Schema<typeof __bknd>;
|
||||||
type T_INTERNAL_EM = {
|
interface T_INTERNAL_EM {
|
||||||
__bknd: ConfigTable2;
|
__bknd: ConfigTable2;
|
||||||
};
|
}
|
||||||
|
|
||||||
// @todo: cleanup old diffs on upgrade
|
// @todo: cleanup old diffs on upgrade
|
||||||
// @todo: cleanup multiple backups on upgrade
|
// @todo: cleanup multiple backups on upgrade
|
||||||
@@ -116,7 +123,7 @@ export class ModuleManager {
|
|||||||
// internal em for __bknd config table
|
// internal em for __bknd config table
|
||||||
__em!: EntityManager<T_INTERNAL_EM>;
|
__em!: EntityManager<T_INTERNAL_EM>;
|
||||||
// ctx for modules
|
// ctx for modules
|
||||||
em!: EntityManager<any>;
|
em!: EntityManager;
|
||||||
server!: Hono;
|
server!: Hono;
|
||||||
emgr!: EventManager;
|
emgr!: EventManager;
|
||||||
guard!: Guard;
|
guard!: Guard;
|
||||||
@@ -294,7 +301,7 @@ export class ModuleManager {
|
|||||||
version,
|
version,
|
||||||
json: configs,
|
json: configs,
|
||||||
updated_at: new Date()
|
updated_at: new Date()
|
||||||
},
|
} as any,
|
||||||
{
|
{
|
||||||
type: "config",
|
type: "config",
|
||||||
version
|
version
|
||||||
@@ -448,6 +455,9 @@ export class ModuleManager {
|
|||||||
await this.buildModules();
|
await this.buildModules();
|
||||||
await this.save();
|
await this.save();
|
||||||
|
|
||||||
|
// run initial setup
|
||||||
|
await this.setupInitial();
|
||||||
|
|
||||||
this.logger.clear();
|
this.logger.clear();
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -462,6 +472,21 @@ export class ModuleManager {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async setupInitial() {
|
||||||
|
const ctx = {
|
||||||
|
...this.ctx(),
|
||||||
|
// disable events for initial setup
|
||||||
|
em: this.ctx().em.fork()
|
||||||
|
};
|
||||||
|
|
||||||
|
// perform a sync
|
||||||
|
await ctx.em.schema().sync({ force: true });
|
||||||
|
await this.options?.seed?.(ctx);
|
||||||
|
|
||||||
|
// run first boot event
|
||||||
|
await this.options?.onFirstBoot?.();
|
||||||
|
}
|
||||||
|
|
||||||
get<K extends keyof Modules>(key: K): Modules[K] {
|
get<K extends keyof Modules>(key: K): Modules[K] {
|
||||||
if (!(key in this.modules)) {
|
if (!(key in this.modules)) {
|
||||||
throw new Error(`Module "${key}" doesn't exist, cannot get`);
|
throw new Error(`Module "${key}" doesn't exist, cannot get`);
|
||||||
|
|||||||
@@ -74,6 +74,21 @@ export class AppServer extends Module<typeof serverConfigSchema> {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// add an initial fallback route
|
||||||
|
this.client.use("/", async (c, next) => {
|
||||||
|
await next();
|
||||||
|
// if not finalized or giving a 404
|
||||||
|
if (!c.finalized || c.res.status === 404) {
|
||||||
|
// double check it's root
|
||||||
|
if (new URL(c.req.url).pathname === "/") {
|
||||||
|
c.res = undefined;
|
||||||
|
c.res = Response.json({
|
||||||
|
bknd: "hello world!"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.client.onError((err, c) => {
|
this.client.onError((err, c) => {
|
||||||
//throw err;
|
//throw err;
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -82,21 +97,6 @@ export class AppServer extends Module<typeof serverConfigSchema> {
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*if (isDebug()) {
|
|
||||||
console.log("accept", c.req.header("Accept"));
|
|
||||||
if (c.req.header("Accept") === "application/json") {
|
|
||||||
const stack = err.stack;
|
|
||||||
|
|
||||||
if ("toJSON" in err && typeof err.toJSON === "function") {
|
|
||||||
return c.json({ ...err.toJSON(), stack }, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.json({ message: String(err), stack }, 500);
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
|
|
||||||
if (err instanceof Exception) {
|
if (err instanceof Exception) {
|
||||||
console.log("---is exception", err.code);
|
console.log("---is exception", err.code);
|
||||||
return c.json(err.toJSON(), err.code as any);
|
return c.json(err.toJSON(), err.code as any);
|
||||||
@@ -107,32 +107,6 @@ export class AppServer extends Module<typeof serverConfigSchema> {
|
|||||||
this.setBuilt();
|
this.setBuilt();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*setAdminHtml(html: string) {
|
|
||||||
this.admin_html = html;
|
|
||||||
const basepath = (String(this.config.admin.basepath) + "/").replace(/\/+$/, "/");
|
|
||||||
|
|
||||||
const allowed_prefix = basepath + "auth";
|
|
||||||
const login_path = basepath + "auth/login";
|
|
||||||
|
|
||||||
this.client.get(basepath + "*", async (c, next) => {
|
|
||||||
const path = new URL(c.req.url).pathname;
|
|
||||||
if (!path.startsWith(allowed_prefix)) {
|
|
||||||
console.log("guard check permissions");
|
|
||||||
try {
|
|
||||||
this.ctx.guard.throwUnlessGranted(SystemPermissions.admin);
|
|
||||||
} catch (e) {
|
|
||||||
return c.redirect(login_path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.html(this.admin_html!);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getAdminHtml() {
|
|
||||||
return this.admin_html;
|
|
||||||
}*/
|
|
||||||
|
|
||||||
override toJSON(secrets?: boolean) {
|
override toJSON(secrets?: boolean) {
|
||||||
return this.config;
|
return this.config;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ const Skeleton = ({ theme = "light" }: { theme?: string }) => {
|
|||||||
className="flex flex-row w-full h-16 gap-2.5 border-muted border-b justify-start bg-muted/10"
|
className="flex flex-row w-full h-16 gap-2.5 border-muted border-b justify-start bg-muted/10"
|
||||||
>
|
>
|
||||||
<div className="max-h-full flex hover:bg-primary/5 link p-2.5 w-[134px] outline-none">
|
<div className="max-h-full flex hover:bg-primary/5 link p-2.5 w-[134px] outline-none">
|
||||||
<Logo />
|
<Logo theme={theme} />
|
||||||
</div>
|
</div>
|
||||||
<nav className="hidden md:flex flex-row gap-2.5 pl-0 p-2.5 items-center">
|
<nav className="hidden md:flex flex-row gap-2.5 pl-0 p-2.5 items-center">
|
||||||
{[...new Array(5)].map((item, key) => (
|
{[...new Array(5)].map((item, key) => (
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import { useApi } from "ui/client";
|
|||||||
|
|
||||||
export const useApiQuery = <
|
export const useApiQuery = <
|
||||||
Data,
|
Data,
|
||||||
RefineFn extends (data: ResponseObject<Data>) => any = (data: ResponseObject<Data>) => Data
|
RefineFn extends (data: ResponseObject<Data>) => unknown = (data: ResponseObject<Data>) => Data
|
||||||
>(
|
>(
|
||||||
fn: (api: Api) => FetchPromise<Data>,
|
fn: (api: Api) => FetchPromise<Data>,
|
||||||
options?: SWRConfiguration & { enabled?: boolean; refine?: RefineFn }
|
options?: SWRConfiguration & { enabled?: boolean; refine?: RefineFn }
|
||||||
) => {
|
) => {
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
const promise = fn(api);
|
const promise = fn(api);
|
||||||
const refine = options?.refine ?? ((data: ResponseObject<Data>) => data);
|
const refine = options?.refine ?? ((data: any) => data);
|
||||||
const fetcher = () => promise.execute().then(refine);
|
const fetcher = () => promise.execute().then(refine);
|
||||||
const key = promise.key();
|
const key = promise.key();
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
import type { DataApi } from "data/api/DataApi";
|
|
||||||
import { useApi } from "ui/client";
|
|
||||||
|
|
||||||
type OmitFirstArg<F> = F extends (x: any, ...args: infer P) => any
|
|
||||||
? (...args: P) => ReturnType<F>
|
|
||||||
: never;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps all DataApi functions and omits
|
|
||||||
* the first argument "entity" for convenience
|
|
||||||
* @param entity
|
|
||||||
*/
|
|
||||||
export const useData = <T extends keyof DataApi>(entity: string) => {
|
|
||||||
const api = useApi().data;
|
|
||||||
const methods = [
|
|
||||||
"readOne",
|
|
||||||
"readMany",
|
|
||||||
"readManyByReference",
|
|
||||||
"createOne",
|
|
||||||
"updateOne",
|
|
||||||
"deleteOne"
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
return methods.reduce(
|
|
||||||
(acc, method) => {
|
|
||||||
// @ts-ignore
|
|
||||||
acc[method] = (...params) => {
|
|
||||||
// @ts-ignore
|
|
||||||
return api[method](entity, ...params);
|
|
||||||
};
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as {
|
|
||||||
[K in (typeof methods)[number]]: OmitFirstArg<(typeof api)[K]>;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,23 +1,40 @@
|
|||||||
import type { PrimaryFieldType } from "core";
|
import type { DB, PrimaryFieldType } from "core";
|
||||||
import { objectTransform } from "core/utils";
|
import { encodeSearch, objectTransform } from "core/utils";
|
||||||
import type { EntityData, RepoQuery } from "data";
|
import type { EntityData, RepoQuery } from "data";
|
||||||
import type { ResponseObject } from "modules/ModuleApi";
|
import type { ModuleApi, ResponseObject } from "modules/ModuleApi";
|
||||||
import useSWR, { type SWRConfiguration } from "swr";
|
import useSWR, { type SWRConfiguration, mutate } from "swr";
|
||||||
import { useApi } from "ui/client";
|
import { type Api, useApi } from "ui/client";
|
||||||
|
|
||||||
export class UseEntityApiError<Payload = any> extends Error {
|
export class UseEntityApiError<Payload = any> extends Error {
|
||||||
constructor(
|
constructor(
|
||||||
public payload: Payload,
|
public response: ResponseObject<Payload>,
|
||||||
public response: Response,
|
fallback?: string
|
||||||
message?: string
|
|
||||||
) {
|
) {
|
||||||
|
let message = fallback;
|
||||||
|
if ("error" in response) {
|
||||||
|
message = response.error as string;
|
||||||
|
if (fallback) {
|
||||||
|
message = `${fallback}: ${message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
super(message ?? "UseEntityApiError");
|
super(message ?? "UseEntityApiError");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Test() {
|
||||||
|
const { read } = useEntity("users");
|
||||||
|
async () => {
|
||||||
|
const data = await read();
|
||||||
|
};
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export const useEntity = <
|
export const useEntity = <
|
||||||
Entity extends string,
|
Entity extends keyof DB | string,
|
||||||
Id extends PrimaryFieldType | undefined = undefined
|
Id extends PrimaryFieldType | undefined = undefined,
|
||||||
|
Data = Entity extends keyof DB ? DB[Entity] : EntityData
|
||||||
>(
|
>(
|
||||||
entity: Entity,
|
entity: Entity,
|
||||||
id?: Id
|
id?: Id
|
||||||
@@ -25,27 +42,30 @@ export const useEntity = <
|
|||||||
const api = useApi().data;
|
const api = useApi().data;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
create: async (input: EntityData) => {
|
create: async (input: Omit<Data, "id">) => {
|
||||||
const res = await api.createOne(entity, input);
|
const res = await api.createOne(entity, input);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new UseEntityApiError(res.data, res.res, "Failed to create entity");
|
throw new UseEntityApiError(res, `Failed to create entity "${entity}"`);
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
read: async (query: Partial<RepoQuery> = {}) => {
|
read: async (query: Partial<RepoQuery> = {}) => {
|
||||||
const res = id ? await api.readOne(entity, id!, query) : await api.readMany(entity, query);
|
const res = id ? await api.readOne(entity, id!, query) : await api.readMany(entity, query);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new UseEntityApiError(res.data, res.res, "Failed to read entity");
|
throw new UseEntityApiError(res as any, `Failed to read entity "${entity}"`);
|
||||||
}
|
}
|
||||||
return res;
|
// must be manually typed
|
||||||
|
return res as unknown as Id extends undefined
|
||||||
|
? ResponseObject<Data[]>
|
||||||
|
: ResponseObject<Data>;
|
||||||
},
|
},
|
||||||
update: async (input: Partial<EntityData>, _id: PrimaryFieldType | undefined = id) => {
|
update: async (input: Partial<Omit<Data, "id">>, _id: PrimaryFieldType | undefined = id) => {
|
||||||
if (!_id) {
|
if (!_id) {
|
||||||
throw new Error("id is required");
|
throw new Error("id is required");
|
||||||
}
|
}
|
||||||
const res = await api.updateOne(entity, _id, input);
|
const res = await api.updateOne(entity, _id, input);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new UseEntityApiError(res.data, res.res, "Failed to update entity");
|
throw new UseEntityApiError(res, `Failed to update entity "${entity}"`);
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
@@ -56,44 +76,67 @@ export const useEntity = <
|
|||||||
|
|
||||||
const res = await api.deleteOne(entity, _id);
|
const res = await api.deleteOne(entity, _id);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new UseEntityApiError(res.data, res.res, "Failed to delete entity");
|
throw new UseEntityApiError(res, `Failed to delete entity "${entity}"`);
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// @todo: try to get from ModuleApi directly
|
||||||
|
export function makeKey(
|
||||||
|
api: ModuleApi,
|
||||||
|
entity: string,
|
||||||
|
id?: PrimaryFieldType,
|
||||||
|
query?: Partial<RepoQuery>
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
"/" +
|
||||||
|
[...(api.options?.basepath?.split("/") ?? []), entity, ...(id ? [id] : [])]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("/") +
|
||||||
|
(query ? "?" + encodeSearch(query) : "")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const useEntityQuery = <
|
export const useEntityQuery = <
|
||||||
Entity extends string,
|
Entity extends keyof DB | string,
|
||||||
Id extends PrimaryFieldType | undefined = undefined
|
Id extends PrimaryFieldType | undefined = undefined
|
||||||
>(
|
>(
|
||||||
entity: Entity,
|
entity: Entity,
|
||||||
id?: Id,
|
id?: Id,
|
||||||
query?: Partial<RepoQuery>,
|
query?: Partial<RepoQuery>,
|
||||||
options?: SWRConfiguration & { enabled?: boolean }
|
options?: SWRConfiguration & { enabled?: boolean; revalidateOnMutate?: boolean }
|
||||||
) => {
|
) => {
|
||||||
const api = useApi().data;
|
const api = useApi().data;
|
||||||
const key =
|
const key = makeKey(api, entity, id, query);
|
||||||
options?.enabled !== false
|
const { read, ...actions } = useEntity<Entity, Id>(entity, id);
|
||||||
? [...(api.options?.basepath?.split("/") ?? []), entity, ...(id ? [id] : [])].filter(
|
|
||||||
Boolean
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
const { read, ...actions } = useEntity(entity, id) as any;
|
|
||||||
const fetcher = () => read(query);
|
const fetcher = () => read(query);
|
||||||
|
|
||||||
type T = Awaited<ReturnType<(typeof api)[Id extends undefined ? "readMany" : "readOne"]>>;
|
type T = Awaited<ReturnType<typeof fetcher>>;
|
||||||
const swr = useSWR<T>(key, fetcher, {
|
const swr = useSWR<T>(options?.enabled === false ? null : key, fetcher as any, {
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
keepPreviousData: false,
|
keepPreviousData: true,
|
||||||
...options
|
...options
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapped = objectTransform(actions, (action) => {
|
const mutateAll = async () => {
|
||||||
if (action === "read") return;
|
const entityKey = makeKey(api, entity);
|
||||||
|
return mutate((key) => typeof key === "string" && key.startsWith(entityKey), undefined, {
|
||||||
|
revalidate: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return async (...args) => {
|
const mapped = objectTransform(actions, (action) => {
|
||||||
return swr.mutate(action(...args)) as any;
|
return async (...args: any) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const res = await action(...args);
|
||||||
|
|
||||||
|
// mutate all keys of entity by default
|
||||||
|
if (options?.revalidateOnMutate !== false) {
|
||||||
|
await mutateAll();
|
||||||
|
}
|
||||||
|
return res;
|
||||||
};
|
};
|
||||||
}) as Omit<ReturnType<typeof useEntity<Entity, Id>>, "read">;
|
}) as Omit<ReturnType<typeof useEntity<Entity, Id>>, "read">;
|
||||||
|
|
||||||
@@ -105,17 +148,62 @@ export const useEntityQuery = <
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export async function mutateEntityCache<
|
||||||
|
Entity extends keyof DB | string,
|
||||||
|
Data = Entity extends keyof DB ? Omit<DB[Entity], "id"> : EntityData
|
||||||
|
>(api: Api["data"], entity: Entity, id: PrimaryFieldType, partialData: Partial<Data>) {
|
||||||
|
function update(prev: any, partialNext: any) {
|
||||||
|
if (
|
||||||
|
typeof prev !== "undefined" &&
|
||||||
|
typeof partialNext !== "undefined" &&
|
||||||
|
"id" in prev &&
|
||||||
|
prev.id === id
|
||||||
|
) {
|
||||||
|
return { ...prev, ...partialNext };
|
||||||
|
}
|
||||||
|
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entityKey = makeKey(api, entity);
|
||||||
|
|
||||||
|
return mutate(
|
||||||
|
(key) => typeof key === "string" && key.startsWith(entityKey),
|
||||||
|
async (data) => {
|
||||||
|
if (typeof data === "undefined") return;
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
return data.map((item) => update(item, partialData));
|
||||||
|
}
|
||||||
|
return update(data, partialData);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
revalidate: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const useEntityMutate = <
|
export const useEntityMutate = <
|
||||||
Entity extends string,
|
Entity extends keyof DB | string,
|
||||||
Id extends PrimaryFieldType | undefined = undefined
|
Id extends PrimaryFieldType | undefined = undefined,
|
||||||
|
Data = Entity extends keyof DB ? Omit<DB[Entity], "id"> : EntityData
|
||||||
>(
|
>(
|
||||||
entity: Entity,
|
entity: Entity,
|
||||||
id?: Id,
|
id?: Id,
|
||||||
options?: SWRConfiguration
|
options?: SWRConfiguration
|
||||||
) => {
|
) => {
|
||||||
const { data, ...$q } = useEntityQuery(entity, id, undefined, {
|
const { data, ...$q } = useEntityQuery<Entity, Id>(entity, id, undefined, {
|
||||||
...options,
|
...options,
|
||||||
enabled: false
|
enabled: false
|
||||||
});
|
});
|
||||||
return $q;
|
|
||||||
|
const _mutate = id
|
||||||
|
? (data) => mutateEntityCache($q.api, entity, id, data)
|
||||||
|
: (id, data) => mutateEntityCache($q.api, entity, id, data);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...$q,
|
||||||
|
mutate: _mutate as unknown as Id extends undefined
|
||||||
|
? (id: PrimaryFieldType, data: Partial<Data>) => Promise<void>
|
||||||
|
: (data: Partial<Data>) => Promise<void>
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export {
|
|||||||
} from "./ClientProvider";
|
} from "./ClientProvider";
|
||||||
|
|
||||||
export * from "./api/use-api";
|
export * from "./api/use-api";
|
||||||
export * from "./api/use-data";
|
|
||||||
export * from "./api/use-entity";
|
export * from "./api/use-entity";
|
||||||
export { useAuth } from "./schema/auth/use-auth";
|
export { useAuth } from "./schema/auth/use-auth";
|
||||||
export { Api } from "../../Api";
|
export { Api } from "../../Api";
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Type, TypeInvalidError, parse, transformObject } from "core/utils";
|
import { Type, TypeInvalidError, parse, transformObject } from "core/utils";
|
||||||
import type { Entity } from "data";
|
import { constructEntity } from "data";
|
||||||
import { AppData } from "data/AppData";
|
|
||||||
import {
|
import {
|
||||||
type TAppDataEntity,
|
type TAppDataEntity,
|
||||||
type TAppDataEntityFields,
|
type TAppDataEntityFields,
|
||||||
@@ -19,7 +18,7 @@ export function useBkndData() {
|
|||||||
|
|
||||||
// @todo: potentially store in ref, so it doesn't get recomputed? or use memo?
|
// @todo: potentially store in ref, so it doesn't get recomputed? or use memo?
|
||||||
const entities = transformObject(config.data.entities ?? {}, (entity, name) => {
|
const entities = transformObject(config.data.entities ?? {}, (entity, name) => {
|
||||||
return AppData.constructEntity(name, entity);
|
return constructEntity(name, entity);
|
||||||
});
|
});
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { App } from "App";
|
import type { App } from "App";
|
||||||
import type { Entity, EntityRelation } from "data";
|
import { type Entity, type EntityRelation, constructEntity, constructRelation } from "data";
|
||||||
import { AppData } from "data/AppData";
|
|
||||||
import { RelationAccessor } from "data/relations/RelationAccessor";
|
import { RelationAccessor } from "data/relations/RelationAccessor";
|
||||||
import { Flow, TaskMap } from "flows";
|
import { Flow, TaskMap } from "flows";
|
||||||
|
|
||||||
@@ -20,11 +19,11 @@ export class AppReduced {
|
|||||||
//console.log("received appjson", appJson);
|
//console.log("received appjson", appJson);
|
||||||
|
|
||||||
this._entities = Object.entries(this.appJson.data.entities ?? {}).map(([name, entity]) => {
|
this._entities = Object.entries(this.appJson.data.entities ?? {}).map(([name, entity]) => {
|
||||||
return AppData.constructEntity(name, entity);
|
return constructEntity(name, entity);
|
||||||
});
|
});
|
||||||
|
|
||||||
this._relations = Object.entries(this.appJson.data.relations ?? {}).map(([, relation]) => {
|
this._relations = Object.entries(this.appJson.data.relations ?? {}).map(([, relation]) => {
|
||||||
return AppData.constructRelation(relation, this.entity.bind(this));
|
return constructRelation(relation, this.entity.bind(this));
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const [name, obj] of Object.entries(this.appJson.flows.flows ?? {})) {
|
for (const [name, obj] of Object.entries(this.appJson.flows.flows ?? {})) {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { ReactCodeMirrorProps } from "@uiw/react-codemirror";
|
import { default as CodeMirror, type ReactCodeMirrorProps } from "@uiw/react-codemirror";
|
||||||
import { Suspense, lazy } from "react";
|
|
||||||
import { useBknd } from "ui/client/bknd";
|
import { useBknd } from "ui/client/bknd";
|
||||||
const CodeMirror = lazy(() => import("@uiw/react-codemirror"));
|
|
||||||
|
|
||||||
export default function CodeEditor({ editable, basicSetup, ...props }: ReactCodeMirrorProps) {
|
export default function CodeEditor({ editable, basicSetup, ...props }: ReactCodeMirrorProps) {
|
||||||
const b = useBknd();
|
const b = useBknd();
|
||||||
@@ -15,13 +14,11 @@ export default function CodeEditor({ editable, basicSetup, ...props }: ReactCode
|
|||||||
: basicSetup;
|
: basicSetup;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense>
|
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
theme={theme === "dark" ? "dark" : "light"}
|
theme={theme === "dark" ? "dark" : "light"}
|
||||||
editable={editable}
|
editable={editable}
|
||||||
basicSetup={_basicSetup}
|
basicSetup={_basicSetup}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import type { Schema } from "@cfworker/json-schema";
|
import type { Schema } from "@cfworker/json-schema";
|
||||||
import Form from "@rjsf/core";
|
import Form from "@rjsf/core";
|
||||||
import type { RJSFSchema, UiSchema } from "@rjsf/utils";
|
import type { RJSFSchema, UiSchema } from "@rjsf/utils";
|
||||||
|
import { cloneDeep } from "lodash-es";
|
||||||
import { forwardRef, useId, useImperativeHandle, useRef, useState } from "react";
|
import { forwardRef, useId, useImperativeHandle, useRef, useState } from "react";
|
||||||
//import { JsonSchemaValidator } from "./JsonSchemaValidator";
|
|
||||||
import { fields as Fields } from "./fields";
|
import { fields as Fields } from "./fields";
|
||||||
import { templates as Templates } from "./templates";
|
import { templates as Templates } from "./templates";
|
||||||
import { widgets as Widgets } from "./widgets";
|
|
||||||
import "./styles.css";
|
|
||||||
import { filterKeys } from "core/utils";
|
|
||||||
import { cloneDeep } from "lodash-es";
|
|
||||||
import { RJSFTypeboxValidator } from "./typebox/RJSFTypeboxValidator";
|
import { RJSFTypeboxValidator } from "./typebox/RJSFTypeboxValidator";
|
||||||
|
import { widgets as Widgets } from "./widgets";
|
||||||
|
|
||||||
const validator = new RJSFTypeboxValidator();
|
const validator = new RJSFTypeboxValidator();
|
||||||
|
|
||||||
|
|||||||
18
app/src/ui/components/form/json-schema/index.tsx
Normal file
18
app/src/ui/components/form/json-schema/index.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Suspense, forwardRef, lazy } from "react";
|
||||||
|
import type { JsonSchemaFormProps, JsonSchemaFormRef } from "./JsonSchemaForm";
|
||||||
|
|
||||||
|
export type { JsonSchemaFormProps, JsonSchemaFormRef };
|
||||||
|
|
||||||
|
const Module = lazy(() =>
|
||||||
|
import("./JsonSchemaForm").then((m) => ({
|
||||||
|
default: m.JsonSchemaForm
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
export const JsonSchemaForm = forwardRef<JsonSchemaFormRef, JsonSchemaFormProps>((props, ref) => {
|
||||||
|
return (
|
||||||
|
<Suspense>
|
||||||
|
<Module ref={ref} {...props} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -42,7 +42,11 @@ const useLocationFromRouter = (router) => {
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Link({ className, ...props }: { className?: string } & LinkProps) {
|
export function Link({
|
||||||
|
className,
|
||||||
|
native,
|
||||||
|
...props
|
||||||
|
}: { className?: string; native?: boolean } & LinkProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [path, navigate] = useLocationFromRouter(router);
|
const [path, navigate] = useLocationFromRouter(router);
|
||||||
|
|
||||||
@@ -55,8 +59,6 @@ export function Link({ className, ...props }: { className?: string } & LinkProps
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClick(e) {}
|
|
||||||
|
|
||||||
const _href = props.href ?? props.to;
|
const _href = props.href ?? props.to;
|
||||||
const href = router
|
const href = router
|
||||||
.hrefs(
|
.hrefs(
|
||||||
@@ -72,6 +74,10 @@ export function Link({ className, ...props }: { className?: string } & LinkProps
|
|||||||
/*if (active) {
|
/*if (active) {
|
||||||
console.log("link", { a, path, absPath, href, to, active, router });
|
console.log("link", { a, path, absPath, href, to, active, router });
|
||||||
}*/
|
}*/
|
||||||
|
if (native) {
|
||||||
|
return <a className={`${active ? "active " : ""}${className}`} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// @ts-expect-error className is not typed on WouterLink
|
// @ts-expect-error className is not typed on WouterLink
|
||||||
<WouterLink className={`${active ? "active " : ""}${className}`} {...props} />
|
<WouterLink className={`${active ? "active " : ""}${className}`} {...props} />
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ function SidebarToggler() {
|
|||||||
export function Header({ hasSidebar = true }) {
|
export function Header({ hasSidebar = true }) {
|
||||||
//const logoReturnPath = "";
|
//const logoReturnPath = "";
|
||||||
const { app } = useBknd();
|
const { app } = useBknd();
|
||||||
const logoReturnPath = app.getAdminConfig().logo_return_path ?? "/";
|
const { logo_return_path = "/", color_scheme = "light" } = app.getAdminConfig();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
@@ -124,11 +124,11 @@ export function Header({ hasSidebar = true }) {
|
|||||||
className="flex flex-row w-full h-16 gap-2.5 border-muted border-b justify-start bg-muted/10"
|
className="flex flex-row w-full h-16 gap-2.5 border-muted border-b justify-start bg-muted/10"
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href={logoReturnPath}
|
href={logo_return_path}
|
||||||
replace
|
native={logo_return_path !== "/"}
|
||||||
className="max-h-full flex hover:bg-primary/5 link p-2.5 w-[134px] outline-none"
|
className="max-h-full flex hover:bg-primary/5 link p-2.5 w-[134px] outline-none"
|
||||||
>
|
>
|
||||||
<Logo />
|
<Logo theme={color_scheme} />
|
||||||
</Link>
|
</Link>
|
||||||
<HeaderNavigation />
|
<HeaderNavigation />
|
||||||
<div className="flex flex-grow" />
|
<div className="flex flex-grow" />
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
JsonSchemaForm,
|
JsonSchemaForm,
|
||||||
type JsonSchemaFormProps,
|
type JsonSchemaFormProps,
|
||||||
type JsonSchemaFormRef
|
type JsonSchemaFormRef
|
||||||
} from "ui/components/form/json-schema/JsonSchemaForm";
|
} from "ui/components/form/json-schema";
|
||||||
|
|
||||||
import type { ContextModalProps } from "@mantine/modals";
|
import type { ContextModalProps } from "@mantine/modals";
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
import type { FieldApi } from "@tanstack/react-form";
|
import type { FieldApi } from "@tanstack/react-form";
|
||||||
import type { EntityData, JsonSchemaField } from "data";
|
import type { EntityData, JsonSchemaField } from "data";
|
||||||
import { Suspense, lazy } from "react";
|
|
||||||
import * as Formy from "ui/components/form/Formy";
|
import * as Formy from "ui/components/form/Formy";
|
||||||
import { FieldLabel } from "ui/components/form/Formy";
|
import { FieldLabel } from "ui/components/form/Formy";
|
||||||
|
import { JsonSchemaForm } from "ui/components/form/json-schema";
|
||||||
const JsonSchemaForm = lazy(() =>
|
|
||||||
import("ui/components/form/json-schema/JsonSchemaForm").then((m) => ({
|
|
||||||
default: m.JsonSchemaForm
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
export function EntityJsonSchemaFormField({
|
export function EntityJsonSchemaFormField({
|
||||||
fieldApi,
|
fieldApi,
|
||||||
@@ -34,7 +28,6 @@ export function EntityJsonSchemaFormField({
|
|||||||
return (
|
return (
|
||||||
<Formy.Group>
|
<Formy.Group>
|
||||||
<FieldLabel htmlFor={fieldApi.name} field={field} />
|
<FieldLabel htmlFor={fieldApi.name} field={field} />
|
||||||
<Suspense fallback={<div>Loading...</div>}>
|
|
||||||
<div
|
<div
|
||||||
data-disabled={disabled ? 1 : undefined}
|
data-disabled={disabled ? 1 : undefined}
|
||||||
className="data-[disabled]:opacity-70 data-[disabled]:pointer-events-none"
|
className="data-[disabled]:opacity-70 data-[disabled]:pointer-events-none"
|
||||||
@@ -50,7 +43,6 @@ export function EntityJsonSchemaFormField({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Suspense>
|
|
||||||
</Formy.Group>
|
</Formy.Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import { Handle, type Node, type NodeProps, Position } from "@xyflow/react";
|
import { Handle, type Node, type NodeProps, Position } from "@xyflow/react";
|
||||||
import { Const, Type, transformObject } from "core/utils";
|
import { Const, Type, transformObject } from "core/utils";
|
||||||
import { type TaskRenderProps, type Trigger, TriggerMap } from "flows";
|
import { type Trigger, TriggerMap } from "flows";
|
||||||
import { Suspense, lazy } from "react";
|
|
||||||
import type { IconType } from "react-icons";
|
import type { IconType } from "react-icons";
|
||||||
import { TbCircleLetterT } from "react-icons/tb";
|
import { TbCircleLetterT } from "react-icons/tb";
|
||||||
const JsonSchemaForm = lazy(() =>
|
import { JsonSchemaForm } from "ui/components/form/json-schema";
|
||||||
import("ui/components/form/json-schema/JsonSchemaForm").then((m) => ({
|
|
||||||
default: m.JsonSchemaForm
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
export type TaskComponentProps = NodeProps<Node<{ trigger: Trigger }>> & {
|
export type TaskComponentProps = NodeProps<Node<{ trigger: Trigger }>> & {
|
||||||
Icon?: IconType;
|
Icon?: IconType;
|
||||||
@@ -48,7 +43,6 @@ export function TriggerComponent({
|
|||||||
</div>
|
</div>
|
||||||
<div className="w-full h-px bg-primary/10" />
|
<div className="w-full h-px bg-primary/10" />
|
||||||
<div className="flex flex-col gap-2 px-3 py-2">
|
<div className="flex flex-col gap-2 px-3 py-2">
|
||||||
<Suspense fallback={<div>Loading...</div>}>
|
|
||||||
<JsonSchemaForm
|
<JsonSchemaForm
|
||||||
className="legacy"
|
className="legacy"
|
||||||
schema={Type.Union(triggerSchemas)}
|
schema={Type.Union(triggerSchemas)}
|
||||||
@@ -58,7 +52,6 @@ export function TriggerComponent({
|
|||||||
/*uiSchema={uiSchema}*/
|
/*uiSchema={uiSchema}*/
|
||||||
/*fields={{ template: TemplateField }}*/
|
/*fields={{ template: TemplateField }}*/
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Handle
|
<Handle
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
import type { Task } from "flows";
|
import type { Task } from "flows";
|
||||||
import { Suspense, lazy } from "react";
|
import { JsonSchemaForm } from "ui/components/form/json-schema";
|
||||||
import { TemplateField } from "./TemplateField";
|
|
||||||
|
|
||||||
const JsonSchemaForm = lazy(() =>
|
|
||||||
import("ui/components/form/json-schema/JsonSchemaForm").then((m) => ({
|
|
||||||
default: m.JsonSchemaForm
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
export type TaskFormProps = {
|
export type TaskFormProps = {
|
||||||
task: Task;
|
task: Task;
|
||||||
@@ -26,7 +19,6 @@ export function TaskForm({ task, onChange, ...props }: TaskFormProps) {
|
|||||||
//console.log("uiSchema", uiSchema);
|
//console.log("uiSchema", uiSchema);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<div>Loading...</div>}>
|
|
||||||
<JsonSchemaForm
|
<JsonSchemaForm
|
||||||
className="legacy"
|
className="legacy"
|
||||||
schema={schema}
|
schema={schema}
|
||||||
@@ -36,6 +28,5 @@ export function TaskForm({ task, onChange, ...props }: TaskFormProps) {
|
|||||||
/*uiSchema={uiSchema}*/
|
/*uiSchema={uiSchema}*/
|
||||||
/*fields={{ template: TemplateField }}*/
|
/*fields={{ template: TemplateField }}*/
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,7 @@ import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
|
|||||||
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
|
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
|
||||||
import { Button } from "ui/components/buttons/Button";
|
import { Button } from "ui/components/buttons/Button";
|
||||||
import { Alert } from "ui/components/display/Alert";
|
import { Alert } from "ui/components/display/Alert";
|
||||||
import {
|
import { JsonSchemaForm, type JsonSchemaFormRef } from "ui/components/form/json-schema";
|
||||||
JsonSchemaForm,
|
|
||||||
type JsonSchemaFormRef
|
|
||||||
} from "ui/components/form/json-schema/JsonSchemaForm";
|
|
||||||
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||||
import { useNavigate } from "ui/lib/routes";
|
import { useNavigate } from "ui/lib/routes";
|
||||||
import { extractSchema } from "../settings/utils/schema";
|
import { extractSchema } from "../settings/utils/schema";
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { cloneDeep, omit } from "lodash-es";
|
import { cloneDeep, omit } from "lodash-es";
|
||||||
import { useBknd } from "ui/client/bknd";
|
import { useBknd } from "ui/client/bknd";
|
||||||
import { Button } from "ui/components/buttons/Button";
|
import { Button } from "ui/components/buttons/Button";
|
||||||
import { JsonSchemaForm } from "ui/components/form/json-schema/JsonSchemaForm";
|
|
||||||
import * as AppShell from "../../layouts/AppShell/AppShell";
|
import * as AppShell from "../../layouts/AppShell/AppShell";
|
||||||
import { extractSchema } from "../settings/utils/schema";
|
|
||||||
|
|
||||||
export function AuthStrategiesList() {
|
export function AuthStrategiesList() {
|
||||||
useBknd({ withSecrets: true });
|
useBknd({ withSecrets: true });
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ export function DataEntityUpdate({ params }) {
|
|||||||
data: {
|
data: {
|
||||||
data: data as any,
|
data: data as any,
|
||||||
entity: entity.toJSON(),
|
entity: entity.toJSON(),
|
||||||
schema: entity.toSchema(true),
|
schema: entity.toSchema({ clean: true }),
|
||||||
form: Form.state.values,
|
form: Form.state.values,
|
||||||
state: Form.state
|
state: Form.state
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,7 @@ import { useBkndData } from "ui/client/schema/data/use-bknd-data";
|
|||||||
import { Button } from "ui/components/buttons/Button";
|
import { Button } from "ui/components/buttons/Button";
|
||||||
import { IconButton } from "ui/components/buttons/IconButton";
|
import { IconButton } from "ui/components/buttons/IconButton";
|
||||||
import { Empty } from "ui/components/display/Empty";
|
import { Empty } from "ui/components/display/Empty";
|
||||||
import {
|
import { JsonSchemaForm, type JsonSchemaFormRef } from "ui/components/form/json-schema";
|
||||||
JsonSchemaForm,
|
|
||||||
type JsonSchemaFormRef
|
|
||||||
} from "ui/components/form/json-schema/JsonSchemaForm";
|
|
||||||
import { Dropdown } from "ui/components/overlay/Dropdown";
|
import { Dropdown } from "ui/components/overlay/Dropdown";
|
||||||
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||||
import { Breadcrumbs2 } from "ui/layouts/AppShell/Breadcrumbs2";
|
import { Breadcrumbs2 } from "ui/layouts/AppShell/Breadcrumbs2";
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { Button } from "ui/components/buttons/Button";
|
|||||||
import { IconButton } from "ui/components/buttons/IconButton";
|
import { IconButton } from "ui/components/buttons/IconButton";
|
||||||
import { JsonViewer } from "ui/components/code/JsonViewer";
|
import { JsonViewer } from "ui/components/code/JsonViewer";
|
||||||
import { MantineSwitch } from "ui/components/form/hook-form-mantine/MantineSwitch";
|
import { MantineSwitch } from "ui/components/form/hook-form-mantine/MantineSwitch";
|
||||||
import { JsonSchemaForm } from "ui/components/form/json-schema/JsonSchemaForm";
|
import { JsonSchemaForm } from "ui/components/form/json-schema";
|
||||||
import { type SortableItemProps, SortableList } from "ui/components/list/SortableList";
|
import { type SortableItemProps, SortableList } from "ui/components/list/SortableList";
|
||||||
import { Popover } from "ui/components/overlay/Popover";
|
import { Popover } from "ui/components/overlay/Popover";
|
||||||
import { fieldSpecs } from "ui/modules/data/components/fields-specs";
|
import { fieldSpecs } from "ui/modules/data/components/fields-specs";
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
import { Suspense, lazy } from "react";
|
import { Suspense, lazy } from "react";
|
||||||
import { useBknd } from "ui/client/bknd";
|
import { useBknd } from "ui/client/bknd";
|
||||||
import { Route, Router, Switch } from "wouter";
|
import { Route, Router, Switch } from "wouter";
|
||||||
|
import AuthRoutes from "./auth";
|
||||||
import { AuthLogin } from "./auth/auth.login";
|
import { AuthLogin } from "./auth/auth.login";
|
||||||
|
import DataRoutes from "./data";
|
||||||
|
import FlowRoutes from "./flows";
|
||||||
|
import MediaRoutes from "./media";
|
||||||
import { Root, RootEmpty } from "./root";
|
import { Root, RootEmpty } from "./root";
|
||||||
|
import SettingsRoutes from "./settings";
|
||||||
|
|
||||||
const DataRoutes = lazy(() => import("./data"));
|
/*const DataRoutes = lazy(() => import("./data"));
|
||||||
const AuthRoutes = lazy(() => import("./auth"));
|
const AuthRoutes = lazy(() => import("./auth"));
|
||||||
const MediaRoutes = lazy(() => import("./media"));
|
const MediaRoutes = lazy(() => import("./media"));
|
||||||
const FlowRoutes = lazy(() => import("./flows"));
|
const FlowRoutes = lazy(() => import("./flows"));
|
||||||
const SettingsRoutes = lazy(() => import("./settings"));
|
const SettingsRoutes = lazy(() => import("./settings"));*/
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const TestRoutes = lazy(() => import("./test"));
|
const TestRoutes = lazy(() => import("./test"));
|
||||||
|
|||||||
@@ -8,10 +8,7 @@ import { Button } from "ui/components/buttons/Button";
|
|||||||
import { IconButton } from "ui/components/buttons/IconButton";
|
import { IconButton } from "ui/components/buttons/IconButton";
|
||||||
import { Alert } from "ui/components/display/Alert";
|
import { Alert } from "ui/components/display/Alert";
|
||||||
import { Empty } from "ui/components/display/Empty";
|
import { Empty } from "ui/components/display/Empty";
|
||||||
import {
|
import { JsonSchemaForm, type JsonSchemaFormRef } from "ui/components/form/json-schema";
|
||||||
JsonSchemaForm,
|
|
||||||
type JsonSchemaFormRef
|
|
||||||
} from "ui/components/form/json-schema/JsonSchemaForm";
|
|
||||||
import { Dropdown } from "ui/components/overlay/Dropdown";
|
import { Dropdown } from "ui/components/overlay/Dropdown";
|
||||||
import { DataTable } from "ui/components/table/DataTable";
|
import { DataTable } from "ui/components/table/DataTable";
|
||||||
import { useEvent } from "ui/hooks/use-event";
|
import { useEvent } from "ui/hooks/use-event";
|
||||||
|
|||||||
@@ -3,16 +3,13 @@ import type { TObject } from "core/utils";
|
|||||||
import { omit } from "lodash-es";
|
import { omit } from "lodash-es";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { TbCirclePlus, TbVariable } from "react-icons/tb";
|
import { TbCirclePlus, TbVariable } from "react-icons/tb";
|
||||||
|
import { useBknd } from "ui/client/BkndProvider";
|
||||||
|
import { Button } from "ui/components/buttons/Button";
|
||||||
|
import * as Formy from "ui/components/form/Formy";
|
||||||
|
import { JsonSchemaForm, type JsonSchemaFormRef } from "ui/components/form/json-schema";
|
||||||
|
import { Dropdown } from "ui/components/overlay/Dropdown";
|
||||||
|
import { Modal } from "ui/components/overlay/Modal";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { useBknd } from "../../../client/BkndProvider";
|
|
||||||
import { Button } from "../../../components/buttons/Button";
|
|
||||||
import * as Formy from "../../../components/form/Formy";
|
|
||||||
import {
|
|
||||||
JsonSchemaForm,
|
|
||||||
type JsonSchemaFormRef
|
|
||||||
} from "../../../components/form/json-schema/JsonSchemaForm";
|
|
||||||
import { Dropdown } from "../../../components/overlay/Dropdown";
|
|
||||||
import { Modal } from "../../../components/overlay/Modal";
|
|
||||||
|
|
||||||
export type SettingsNewModalProps = {
|
export type SettingsNewModalProps = {
|
||||||
schema: TObject;
|
schema: TObject;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { parse } from "core/utils";
|
|||||||
import { AppFlows } from "flows/AppFlows";
|
import { AppFlows } from "flows/AppFlows";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { JsonViewer } from "../../../components/code/JsonViewer";
|
import { JsonViewer } from "../../../components/code/JsonViewer";
|
||||||
import { JsonSchemaForm } from "../../../components/form/json-schema/JsonSchemaForm";
|
import { JsonSchemaForm } from "../../../components/form/json-schema";
|
||||||
import { Scrollable } from "../../../layouts/AppShell/AppShell";
|
import { Scrollable } from "../../../layouts/AppShell/AppShell";
|
||||||
|
|
||||||
export default function FlowCreateSchemaTest() {
|
export default function FlowCreateSchemaTest() {
|
||||||
|
|||||||
@@ -2,12 +2,9 @@ import Form from "@rjsf/core";
|
|||||||
import type { RJSFSchema, UiSchema } from "@rjsf/utils";
|
import type { RJSFSchema, UiSchema } from "@rjsf/utils";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { TbPlus, TbTrash } from "react-icons/tb";
|
import { TbPlus, TbTrash } from "react-icons/tb";
|
||||||
import { Button } from "../../../../components/buttons/Button";
|
import { Button } from "ui/components/buttons/Button";
|
||||||
|
import { JsonSchemaForm, type JsonSchemaFormRef } from "ui/components/form/json-schema";
|
||||||
import * as Formy from "../../../../components/form/Formy";
|
import * as Formy from "../../../../components/form/Formy";
|
||||||
import {
|
|
||||||
JsonSchemaForm,
|
|
||||||
type JsonSchemaFormRef
|
|
||||||
} from "../../../../components/form/json-schema/JsonSchemaForm";
|
|
||||||
import * as AppShell from "../../../../layouts/AppShell/AppShell";
|
import * as AppShell from "../../../../layouts/AppShell/AppShell";
|
||||||
|
|
||||||
class CfJsonSchemaValidator {}
|
class CfJsonSchemaValidator {}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Schema } from "@cfworker/json-schema";
|
import type { Schema } from "@cfworker/json-schema";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { JsonSchemaForm } from "../../../components/form/json-schema/JsonSchemaForm";
|
import { JsonSchemaForm } from "ui/components/form/json-schema";
|
||||||
import { Scrollable } from "../../../layouts/AppShell/AppShell";
|
import { Scrollable } from "ui/layouts/AppShell/AppShell";
|
||||||
|
|
||||||
const schema: Schema = {
|
const schema: Schema = {
|
||||||
definitions: {
|
definitions: {
|
||||||
@@ -9,52 +9,52 @@ const schema: Schema = {
|
|||||||
anyOf: [
|
anyOf: [
|
||||||
{
|
{
|
||||||
title: "String",
|
title: "String",
|
||||||
type: "string",
|
type: "string"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Number",
|
title: "Number",
|
||||||
type: "number",
|
type: "number"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Boolean",
|
title: "Boolean",
|
||||||
type: "boolean",
|
type: "boolean"
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
numeric: {
|
numeric: {
|
||||||
anyOf: [
|
anyOf: [
|
||||||
{
|
{
|
||||||
title: "Number",
|
title: "Number",
|
||||||
type: "number",
|
type: "number"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Datetime",
|
title: "Datetime",
|
||||||
type: "string",
|
type: "string",
|
||||||
format: "date-time",
|
format: "date-time"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Date",
|
title: "Date",
|
||||||
type: "string",
|
type: "string",
|
||||||
format: "date",
|
format: "date"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Time",
|
title: "Time",
|
||||||
type: "string",
|
type: "string",
|
||||||
format: "time",
|
format: "time"
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
boolean: {
|
boolean: {
|
||||||
title: "Boolean",
|
title: "Boolean",
|
||||||
type: "boolean",
|
type: "boolean"
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
operand: {
|
operand: {
|
||||||
enum: ["$and", "$or"],
|
enum: ["$and", "$or"],
|
||||||
default: "$and",
|
default: "$and",
|
||||||
type: "string",
|
type: "string"
|
||||||
},
|
},
|
||||||
conditions: {
|
conditions: {
|
||||||
type: "array",
|
type: "array",
|
||||||
@@ -64,10 +64,10 @@ const schema: Schema = {
|
|||||||
operand: {
|
operand: {
|
||||||
enum: ["$and", "$or"],
|
enum: ["$and", "$or"],
|
||||||
default: "$and",
|
default: "$and",
|
||||||
type: "string",
|
type: "string"
|
||||||
},
|
},
|
||||||
key: {
|
key: {
|
||||||
type: "string",
|
type: "string"
|
||||||
},
|
},
|
||||||
operator: {
|
operator: {
|
||||||
type: "array",
|
type: "array",
|
||||||
@@ -78,30 +78,30 @@ const schema: Schema = {
|
|||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
$eq: {
|
$eq: {
|
||||||
$ref: "#/definitions/primitive",
|
$ref: "#/definitions/primitive"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
required: ["$eq"]
|
||||||
required: ["$eq"],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Lower than",
|
title: "Lower than",
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
$lt: {
|
$lt: {
|
||||||
$ref: "#/definitions/numeric",
|
$ref: "#/definitions/numeric"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
required: ["$lt"]
|
||||||
required: ["$lt"],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Greather than",
|
title: "Greather than",
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
$gt: {
|
$gt: {
|
||||||
$ref: "#/definitions/numeric",
|
$ref: "#/definitions/numeric"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
required: ["$gt"]
|
||||||
required: ["$gt"],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Between",
|
title: "Between",
|
||||||
@@ -110,13 +110,13 @@ const schema: Schema = {
|
|||||||
$between: {
|
$between: {
|
||||||
type: "array",
|
type: "array",
|
||||||
items: {
|
items: {
|
||||||
$ref: "#/definitions/numeric",
|
$ref: "#/definitions/numeric"
|
||||||
},
|
},
|
||||||
minItems: 2,
|
minItems: 2,
|
||||||
maxItems: 2,
|
maxItems: 2
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
required: ["$between"]
|
||||||
required: ["$between"],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "In",
|
title: "In",
|
||||||
@@ -125,23 +125,23 @@ const schema: Schema = {
|
|||||||
$in: {
|
$in: {
|
||||||
type: "array",
|
type: "array",
|
||||||
items: {
|
items: {
|
||||||
$ref: "#/definitions/primitive",
|
$ref: "#/definitions/primitive"
|
||||||
},
|
},
|
||||||
minItems: 1,
|
minItems: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
minItems: 1
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
required: ["key", "operator"]
|
||||||
},
|
},
|
||||||
],
|
minItems: 1
|
||||||
|
}
|
||||||
},
|
},
|
||||||
minItems: 1,
|
required: ["operand", "conditions"]
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["key", "operator"],
|
|
||||||
},
|
|
||||||
minItems: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["operand", "conditions"],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function QueryJsonFormTest() {
|
export default function QueryJsonFormTest() {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import { useBknd } from "../../../client/BkndProvider";
|
import { useBknd } from "ui/client/BkndProvider";
|
||||||
import { JsonSchemaForm } from "../../../components/form/json-schema/JsonSchemaForm";
|
import { JsonSchemaForm } from "ui/components/form/json-schema";
|
||||||
import { Scrollable } from "../../../layouts/AppShell/AppShell";
|
import { Scrollable } from "ui/layouts/AppShell/AppShell";
|
||||||
|
|
||||||
function useSchema() {
|
function useSchema() {
|
||||||
const [schema, setSchema] = useState<any>();
|
const [schema, setSchema] = useState<any>();
|
||||||
|
|||||||
@@ -1,7 +1,20 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useApiQuery } from "ui/client";
|
import { useApi, useApiQuery } from "ui/client";
|
||||||
import { Scrollable } from "ui/layouts/AppShell/AppShell";
|
import { Scrollable } from "ui/layouts/AppShell/AppShell";
|
||||||
|
|
||||||
|
function Bla() {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const one = await api.data.readOne("users", 1);
|
||||||
|
const many = await api.data.readMany("users");
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export default function SWRAndAPI() {
|
export default function SWRAndAPI() {
|
||||||
const [text, setText] = useState("");
|
const [text, setText] = useState("");
|
||||||
const { data, ...r } = useApiQuery((api) => api.data.readOne("comments", 1), {
|
const { data, ...r } = useApiQuery((api) => api.data.readOne("comments", 1), {
|
||||||
@@ -16,7 +29,7 @@ export default function SWRAndAPI() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Scrollable>
|
<Scrollable>
|
||||||
<pre>{JSON.stringify(r.promise.keyArray({ search: false }))}</pre>
|
<pre>{JSON.stringify(r.key)}</pre>
|
||||||
{r.error && <div>failed to load</div>}
|
{r.error && <div>failed to load</div>}
|
||||||
{r.isLoading && <div>loading...</div>}
|
{r.isLoading && <div>loading...</div>}
|
||||||
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
|
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
|
||||||
@@ -26,12 +39,12 @@ export default function SWRAndAPI() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!comment) return;
|
if (!comment) return;
|
||||||
|
|
||||||
await r.mutate(async () => {
|
/*await r.mutate(async () => {
|
||||||
const res = await r.api.data.updateOne("comments", comment.id, {
|
const res = await r.api.data.updateOne("comments", comment.id, {
|
||||||
content: text
|
content: text
|
||||||
});
|
});
|
||||||
return res.data;
|
return res.data;
|
||||||
});
|
});*/
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,54 +1,72 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useEntity, useEntityQuery } from "ui/client/api/use-entity";
|
import { useEntity, useEntityMutate, useEntityQuery } from "ui/client/api/use-entity";
|
||||||
import { Scrollable } from "ui/layouts/AppShell/AppShell";
|
import { Scrollable } from "ui/layouts/AppShell/AppShell";
|
||||||
|
|
||||||
export default function SwrAndDataApi() {
|
export default function SwrAndDataApi() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<Scrollable>
|
||||||
|
asdf
|
||||||
<DirectDataApi />
|
<DirectDataApi />
|
||||||
<QueryDataApi />
|
<QueryDataApi />
|
||||||
</div>
|
<QueryMutateDataApi />
|
||||||
|
</Scrollable>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function QueryDataApi() {
|
function QueryMutateDataApi() {
|
||||||
const [text, setText] = useState("");
|
const { mutate } = useEntityMutate("comments");
|
||||||
const { data, update, ...r } = useEntityQuery("comments", 1, {});
|
const { data, ...r } = useEntityQuery("comments", undefined, {
|
||||||
const comment = data ? data : null;
|
limit: 2
|
||||||
|
});
|
||||||
useEffect(() => {
|
|
||||||
setText(comment?.content ?? "");
|
|
||||||
}, [comment]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Scrollable>
|
<div>
|
||||||
|
bla
|
||||||
<pre>{JSON.stringify(r.key)}</pre>
|
<pre>{JSON.stringify(r.key)}</pre>
|
||||||
{r.error && <div>failed to load</div>}
|
{r.error && <div>failed to load</div>}
|
||||||
{r.isLoading && <div>loading...</div>}
|
{r.isLoading && <div>loading...</div>}
|
||||||
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
|
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
|
||||||
{data && (
|
{data && (
|
||||||
<form
|
<div>
|
||||||
onSubmit={async (e) => {
|
{data.map((comment) => (
|
||||||
e.preventDefault();
|
<input
|
||||||
if (!comment) return;
|
key={String(comment.id)}
|
||||||
await update({ content: text });
|
type="text"
|
||||||
return false;
|
value={comment.content}
|
||||||
|
onChange={async (e) => {
|
||||||
|
await mutate(comment.id, { content: e.target.value });
|
||||||
}}
|
}}
|
||||||
>
|
className="border border-black"
|
||||||
<input type="text" value={text} onChange={(e) => setText(e.target.value)} />
|
/>
|
||||||
<button type="submit">submit</button>
|
))}
|
||||||
</form>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Scrollable>
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function QueryDataApi() {
|
||||||
|
const { data, update, ...r } = useEntityQuery("comments", undefined, {
|
||||||
|
sort: { by: "id", dir: "asc" },
|
||||||
|
limit: 3
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<pre>{JSON.stringify(r.key)}</pre>
|
||||||
|
{r.error && <div>failed to load</div>}
|
||||||
|
{r.isLoading && <div>loading...</div>}
|
||||||
|
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DirectDataApi() {
|
function DirectDataApi() {
|
||||||
const [data, setData] = useState<any>();
|
const [data, setData] = useState<any>();
|
||||||
const { create, read, update, _delete } = useEntity("comments", 1);
|
const { create, read, update, _delete } = useEntity("comments");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
read().then(setData);
|
read().then((data) => setData(data));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <pre>{JSON.stringify(data, null, 2)}</pre>;
|
return <pre>{JSON.stringify(data, null, 2)}</pre>;
|
||||||
|
|||||||
@@ -26,14 +26,13 @@
|
|||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"outDir": "./dist",
|
"outDir": "./dist/types",
|
||||||
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo",
|
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"*": ["./src/*"],
|
"*": ["./src/*"],
|
||||||
"bknd": ["./src/*"]
|
"bknd": ["./src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["./src/**/*.ts", "./src/**/*.tsx", "./env.d.ts"],
|
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
|
||||||
"exclude": ["node_modules", "dist/**/*", "../examples/bun"]
|
"exclude": ["node_modules", "dist", "dist/types", "**/*.d.ts"]
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,10 @@
|
|||||||
import { serveStatic } from "@hono/node-server/serve-static";
|
import { serveStatic } from "@hono/node-server/serve-static";
|
||||||
import { createClient } from "@libsql/client/node";
|
import { createClient } from "@libsql/client/node";
|
||||||
import { App } from "./src";
|
import { App, registries } from "./src";
|
||||||
import { LibsqlConnection } from "./src/data";
|
import { LibsqlConnection } from "./src/data";
|
||||||
import { StorageLocalAdapter } from "./src/media/storage/adapters/StorageLocalAdapter";
|
import { StorageLocalAdapter } from "./src/media/storage/adapters/StorageLocalAdapter";
|
||||||
import { registries } from "./src/modules/registries";
|
|
||||||
|
|
||||||
registries.media.add("local", {
|
registries.media.register("local", StorageLocalAdapter);
|
||||||
cls: StorageLocalAdapter,
|
|
||||||
schema: StorageLocalAdapter.prototype.getSchema()
|
|
||||||
});
|
|
||||||
|
|
||||||
const credentials = {
|
const credentials = {
|
||||||
url: import.meta.env.VITE_DB_URL!,
|
url: import.meta.env.VITE_DB_URL!,
|
||||||
@@ -24,8 +20,8 @@ export default {
|
|||||||
async fetch(request: Request) {
|
async fetch(request: Request) {
|
||||||
const app = App.create({ connection });
|
const app = App.create({ connection });
|
||||||
|
|
||||||
app.emgr.on(
|
app.emgr.onEvent(
|
||||||
"app-built",
|
App.Events.AppBuiltEvent,
|
||||||
async () => {
|
async () => {
|
||||||
app.registerAdminController({ forceDev: true });
|
app.registerAdminController({ forceDev: true });
|
||||||
app.module.server.client.get("/assets/*", serveStatic({ root: "./" }));
|
app.module.server.client.get("/assets/*", serveStatic({ root: "./" }));
|
||||||
|
|||||||
@@ -45,12 +45,14 @@ export const ALL = serve({
|
|||||||
connection: {
|
connection: {
|
||||||
type: "libsql",
|
type: "libsql",
|
||||||
config: {
|
config: {
|
||||||
url: "file:data.db"
|
// location of your local Astro DB
|
||||||
|
// make sure to use a remote URL in production
|
||||||
|
url: "file:.astro/content.db"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
For more information about the connection object, refer to the [Setup](/setup) guide. In the
|
For more information about the connection object, refer to the [Setup](/setup/introduction) guide. In the
|
||||||
special case of astro, you may also use your Astro DB credentials since it's also using LibSQL
|
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.
|
under the hood. Refer to the [Astro DB documentation](https://docs.astro.build/en/guides/astro-db/) for more information.
|
||||||
|
|
||||||
@@ -73,7 +75,11 @@ export const prerender = false;
|
|||||||
<body>
|
<body>
|
||||||
<Admin
|
<Admin
|
||||||
withProvider={{ user }}
|
withProvider={{ user }}
|
||||||
config={{ basepath: "/admin", color_scheme: "dark" }}
|
config={{
|
||||||
|
basepath: "/admin",
|
||||||
|
color_scheme: "dark",
|
||||||
|
logo_return_path: "/../"
|
||||||
|
}}
|
||||||
client:only
|
client:only
|
||||||
/>
|
/>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ serve({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
For more information about the connection object, refer to the [Setup](/setup) guide.
|
For more information about the connection object, refer to the [Setup](/setup/introduction) guide.
|
||||||
|
|
||||||
Run the application using Bun by executing:
|
Run the application using Bun by executing:
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ and then install bknd as a dependency:
|
|||||||
|
|
||||||
|
|
||||||
## Serve the API
|
## Serve the API
|
||||||
|
If you don't choose anything specific, the following code will use the `warm` mode. See the
|
||||||
|
chapter [Using a different mode](#using-a-different-mode) for available modes.
|
||||||
|
|
||||||
``` ts
|
``` ts
|
||||||
import { serve } from "bknd/adapter/cloudflare";
|
import { serve } from "bknd/adapter/cloudflare";
|
||||||
|
|
||||||
export default serve(
|
export default serve({
|
||||||
{
|
|
||||||
app: (env: Env) => ({
|
app: (env: Env) => ({
|
||||||
connection: {
|
connection: {
|
||||||
type: "libsql",
|
type: "libsql",
|
||||||
@@ -27,10 +29,9 @@ export default serve(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
});
|
||||||
);
|
|
||||||
```
|
```
|
||||||
For more information about the connection object, refer to the [Setup](/setup) guide.
|
For more information about the connection object, refer to the [Setup](/setup/introduction) guide.
|
||||||
|
|
||||||
Now run the worker:
|
Now run the worker:
|
||||||
```bash
|
```bash
|
||||||
@@ -49,12 +50,11 @@ bucket = "node_modules/bknd/dist/static"
|
|||||||
```
|
```
|
||||||
|
|
||||||
And then modify the worker entry as follows:
|
And then modify the worker entry as follows:
|
||||||
``` ts {2, 15, 17}
|
``` ts {2, 14, 15}
|
||||||
import { serve } from "bknd/adapter/cloudflare";
|
import { serve } from "bknd/adapter/cloudflare";
|
||||||
import manifest from "__STATIC_CONTENT_MANIFEST";
|
import manifest from "__STATIC_CONTENT_MANIFEST";
|
||||||
|
|
||||||
export default serve(
|
export default serve({
|
||||||
{
|
|
||||||
app: (env: Env) => ({
|
app: (env: Env) => ({
|
||||||
connection: {
|
connection: {
|
||||||
type: "libsql",
|
type: "libsql",
|
||||||
@@ -64,20 +64,18 @@ export default serve(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
manifest,
|
||||||
setAdminHtml: true
|
setAdminHtml: true
|
||||||
},
|
});
|
||||||
manifest
|
|
||||||
);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Adding custom routes
|
## Adding custom routes
|
||||||
You can also add custom routes by defining them after the app has been built, like so:
|
You can also add custom routes by defining them after the app has been built, like so:
|
||||||
```ts {15-17}
|
```ts {14-16}
|
||||||
import { serve } from "bknd/adapter/cloudflare";
|
import { serve } from "bknd/adapter/cloudflare";
|
||||||
import manifest from "__STATIC_CONTENT_MANIFEST";
|
import manifest from "__STATIC_CONTENT_MANIFEST";
|
||||||
|
|
||||||
export default serve(
|
export default serve({
|
||||||
{
|
|
||||||
app: (env: Env) => ({
|
app: (env: Env) => ({
|
||||||
connection: {
|
connection: {
|
||||||
type: "libsql",
|
type: "libsql",
|
||||||
@@ -90,8 +88,107 @@ export default serve(
|
|||||||
onBuilt: async (app) => {
|
onBuilt: async (app) => {
|
||||||
app.modules.server.get("/hello", (c) => c.json({ hello: "world" }));
|
app.modules.server.get("/hello", (c) => c.json({ hello: "world" }));
|
||||||
},
|
},
|
||||||
|
manifest,
|
||||||
setAdminHtml: true
|
setAdminHtml: true
|
||||||
},
|
});
|
||||||
manifest
|
```
|
||||||
);
|
|
||||||
|
## Using a different mode
|
||||||
|
With the Cloudflare Workers adapter, you're being offered to 4 modes to choose from (default:
|
||||||
|
`warm`):
|
||||||
|
|
||||||
|
| Mode | Description | Use Case |
|
||||||
|
|:----------|:-------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| `fresh` | On every request, the configuration gets refetched, app built and then served. | Ideal if you don't want to deal with eviction, KV or Durable Objects. |
|
||||||
|
| `warm` | It tries to keep the built app in memory for as long as possible, and rebuilds if evicted. | Better response times, should be the default choice. |
|
||||||
|
| `cache` | The configuration is fetched from KV to reduce the initial roundtrip to the database. | Generally faster response times with irregular access patterns. |
|
||||||
|
| `durable` | The bknd app is ran inside a Durable Object and can be configured to stay alive. | Slowest boot time, but fastest responses. Can be kept alive for as long as you want, giving similar response times as server instances. |
|
||||||
|
|
||||||
|
### Modes: `fresh` and `warm`
|
||||||
|
To use either `fresh` or `warm`, all you have to do is adding the desired mode to `cloudflare.
|
||||||
|
mode`, like so:
|
||||||
|
```ts
|
||||||
|
import { serve } from "bknd/adapter/cloudflare";
|
||||||
|
|
||||||
|
export default serve({
|
||||||
|
/* ... */,
|
||||||
|
mode: "fresh" // mode: "fresh" | "warm" | "cache" | "durable"
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mode: `cache`
|
||||||
|
For the cache mode to work, you also need to specify the KV to be used. For this, use the
|
||||||
|
`bindings` property:
|
||||||
|
```ts
|
||||||
|
import { serve } from "bknd/adapter/cloudflare";
|
||||||
|
|
||||||
|
export default serve({
|
||||||
|
/* ... */,
|
||||||
|
mode: "cache",
|
||||||
|
bindings: (env: Env) => ({ kv: env.KV })
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mode: `durable` (advanced)
|
||||||
|
To use the `durable` mode, you have to specify the Durable Object to extract from your
|
||||||
|
environment, and additionally export the `DurableBkndApp` class:
|
||||||
|
```ts
|
||||||
|
import { serve, DurableBkndApp } from "bknd/adapter/cloudflare";
|
||||||
|
|
||||||
|
export { DurableBkndApp };
|
||||||
|
export default serve({
|
||||||
|
/* ... */,
|
||||||
|
mode: "durable",
|
||||||
|
bindings: (env: Env) => ({ dobj: env.DOBJ }),
|
||||||
|
keepAliveSeconds: 60 // optional
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, you need to define the Durable Object in your `wrangler.toml` file (refer to the [Durable
|
||||||
|
Objects](https://developers.cloudflare.com/durable-objects/) documentation):
|
||||||
|
```toml
|
||||||
|
[[durable_objects.bindings]]
|
||||||
|
name = "DOBJ"
|
||||||
|
class_name = "DurableBkndApp"
|
||||||
|
|
||||||
|
[[migrations]]
|
||||||
|
tag = "v1"
|
||||||
|
new_classes = ["DurableBkndApp"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Since the communication between the Worker and Durable Object is serialized, the `onBuilt`
|
||||||
|
property won't work. To use it (e.g. to specify special routes), you need to extend from the
|
||||||
|
`DurableBkndApp`:
|
||||||
|
```ts
|
||||||
|
import type { App } from "bknd";
|
||||||
|
import { serve, DurableBkndApp } from "bknd/adapter/cloudflare";
|
||||||
|
|
||||||
|
export default serve({
|
||||||
|
/* ... */,
|
||||||
|
mode: "durable",
|
||||||
|
bindings: (env: Env) => ({ dobj: env.DOBJ }),
|
||||||
|
keepAliveSeconds: 60 // optional
|
||||||
|
});
|
||||||
|
|
||||||
|
export class CustomDurableBkndApp extends DurableBkndApp {
|
||||||
|
async onBuilt(app: App) {
|
||||||
|
app.modules.server.get("/custom/endpoint", (c) => c.text("Custom"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
In case you've already deployed your Worker, the deploy command may complain about a new class
|
||||||
|
being used. To fix this issue, you need to add a "rename migration":
|
||||||
|
```toml
|
||||||
|
[[durable_objects.bindings]]
|
||||||
|
name = "DOBJ"
|
||||||
|
class_name = "CustomDurableBkndApp"
|
||||||
|
|
||||||
|
[[migrations]]
|
||||||
|
tag = "v1"
|
||||||
|
new_classes = ["DurableBkndApp"]
|
||||||
|
|
||||||
|
[[migrations]]
|
||||||
|
tag = "v2"
|
||||||
|
renamed_classes = [{from = "DurableBkndApp", to = "CustomDurableBkndApp"}]
|
||||||
|
deleted_classes = ["DurableBkndApp"]
|
||||||
```
|
```
|
||||||
@@ -14,7 +14,7 @@ Install bknd as a dependency:
|
|||||||
import { serve } from "bknd/adapter/nextjs";
|
import { serve } from "bknd/adapter/nextjs";
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
runtime: "experimental-edge",
|
runtime: "experimental-edge", // or "edge", depending on your nextjs version
|
||||||
unstable_allowDynamic: ["**/*.js"]
|
unstable_allowDynamic: ["**/*.js"]
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -28,12 +28,13 @@ export default serve({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
For more information about the connection object, refer to the [Setup](/setup) guide.
|
For more information about the connection object, refer to the [Setup](/setup/introduction) guide.
|
||||||
|
|
||||||
## Enabling the Admin UI
|
## Enabling the Admin UI
|
||||||
Create a file `[[...admin]].tsx` inside the `pages/admin` folder:
|
Create a file `[[...admin]].tsx` inside the `pages/admin` folder:
|
||||||
```tsx
|
```tsx
|
||||||
// pages/admin/[[...admin]].tsx
|
// pages/admin/[[...admin]].tsx
|
||||||
|
import type { InferGetServerSidePropsType as InferProps } from "next";
|
||||||
import { withApi } from "bknd/adapter/nextjs";
|
import { withApi } from "bknd/adapter/nextjs";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import "bknd/dist/styles.css";
|
import "bknd/dist/styles.css";
|
||||||
@@ -50,9 +51,12 @@ export const getServerSideProps = withApi(async (context) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function AdminPage() {
|
export default function AdminPage({ user }: InferProps<typeof getServerSideProps>) {
|
||||||
if (typeof document === "undefined") return null;
|
if (typeof document === "undefined") return null;
|
||||||
return <Admin withProvider config={{ basepath: "/admin" }} />;
|
return <Admin
|
||||||
|
withProvider={{ user }}
|
||||||
|
config={{ basepath: "/admin", logo_return_path: "/../" }}
|
||||||
|
/>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const config = {
|
|||||||
|
|
||||||
serve(config);
|
serve(config);
|
||||||
```
|
```
|
||||||
For more information about the connection object, refer to the [Setup](/setup) guide.
|
For more information about the connection object, refer to the [Setup](/setup/introduction) guide.
|
||||||
|
|
||||||
Run the application using node by executing:
|
Run the application using node by executing:
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const handler = serve({
|
|||||||
export const loader = handler;
|
export const loader = handler;
|
||||||
export const action = handler;
|
export const action = handler;
|
||||||
```
|
```
|
||||||
For more information about the connection object, refer to the [Setup](/setup) guide.
|
For more information about the connection object, refer to the [Setup](/setup/introduction) guide.
|
||||||
|
|
||||||
Now make sure that you wrap your root layout with the `ClientProvider` so that all components
|
Now make sure that you wrap your root layout with the `ClientProvider` so that all components
|
||||||
share the same context:
|
share the same context:
|
||||||
|
|||||||
@@ -61,7 +61,11 @@
|
|||||||
"navigation": [
|
"navigation": [
|
||||||
{
|
{
|
||||||
"group": "Getting Started",
|
"group": "Getting Started",
|
||||||
"pages": ["introduction", "setup", "sdk", "react", "cli"]
|
"pages": ["introduction", "sdk", "react", "cli"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Setup",
|
||||||
|
"pages": ["setup/introduction", "setup/database"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "Modules",
|
"group": "Modules",
|
||||||
@@ -74,39 +78,17 @@
|
|||||||
"modules/flows"
|
"modules/flows"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"group": "Configuration",
|
|
||||||
"pages": [
|
|
||||||
"config/overview",
|
|
||||||
"config/migration",
|
|
||||||
{
|
|
||||||
"group": "Modules",
|
|
||||||
"pages": [
|
|
||||||
"config/modules/overview",
|
|
||||||
"config/modules/server",
|
|
||||||
"config/modules/data",
|
|
||||||
"config/modules/auth",
|
|
||||||
"config/modules/flows",
|
|
||||||
"config/modules/media"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"group": "Integration",
|
"group": "Integration",
|
||||||
"pages": [
|
"pages": [
|
||||||
"integration/extending",
|
"integration/extending",
|
||||||
"integration/hono",
|
|
||||||
"integration/nextjs",
|
"integration/nextjs",
|
||||||
"integration/remix",
|
"integration/remix",
|
||||||
"integration/cloudflare",
|
"integration/cloudflare",
|
||||||
"integration/bun",
|
"integration/bun",
|
||||||
"integration/vite",
|
|
||||||
"integration/express",
|
|
||||||
"integration/astro",
|
"integration/astro",
|
||||||
"integration/node",
|
"integration/node",
|
||||||
"integration/deno",
|
"integration/deno",
|
||||||
"integration/browser",
|
|
||||||
"integration/docker"
|
"integration/docker"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
183
docs/setup/database.mdx
Normal file
183
docs/setup/database.mdx
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
---
|
||||||
|
title: 'Database'
|
||||||
|
description: 'Choosing the right database configuration'
|
||||||
|
---
|
||||||
|
|
||||||
|
In order to use **bknd**, you need to prepare access information to your database and install
|
||||||
|
the dependencies.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Connections to the database are managed using Kysely. Therefore, all its dialects are
|
||||||
|
theoretically supported. However, only the `SQLite` dialect is implemented as of now.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
## Database
|
||||||
|
### SQLite as file
|
||||||
|
The easiest to get started is using SQLite as a file. When serving the API in the "Integrations",
|
||||||
|
the function accepts an object with connection details. To use a file, use the following:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "libsql",
|
||||||
|
"config": {
|
||||||
|
"url": "file:<path/to/your/database.db>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Please note that using SQLite as a file is only supported in server environments.
|
||||||
|
|
||||||
|
### SQLite using LibSQL
|
||||||
|
Turso offers a SQLite-fork called LibSQL that runs a server around your SQLite database. To
|
||||||
|
point **bknd** to a local instance of LibSQL, [install Turso's CLI](https://docs.turso.tech/cli/introduction) and run the following command:
|
||||||
|
```bash
|
||||||
|
turso dev
|
||||||
|
```
|
||||||
|
|
||||||
|
The command will yield a URL. Use it in the connection object:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "libsql",
|
||||||
|
"config": {
|
||||||
|
"url": "http://localhost:8080"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### SQLite using LibSQL on Turso
|
||||||
|
If you want to use LibSQL on Turso, [sign up for a free account](https://turso.tech/), create a database and point your
|
||||||
|
connection object to your new database:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "libsql",
|
||||||
|
"config": {
|
||||||
|
"url": "libsql://your-database-url.turso.io",
|
||||||
|
"authToken": "your-auth-token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Connection
|
||||||
|
<Note>
|
||||||
|
Follow the progress of custom connections on its [Github Issue](https://github.com/bknd-io/bknd/issues/24).
|
||||||
|
If you're interested, make sure to upvote so it can be prioritized.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
Any bknd app instantiation accepts as connection either `undefined`, a connection object like
|
||||||
|
described above, or an class instance that extends from `Connection`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { createApp } from "bknd";
|
||||||
|
import { Connection } from "bknd/data";
|
||||||
|
|
||||||
|
class CustomConnection extends Connection {
|
||||||
|
constructor() {
|
||||||
|
const kysely = new Kysely(/* ... */);
|
||||||
|
super(kysely);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const connection = new CustomConnection();
|
||||||
|
|
||||||
|
// e.g. and then, create an instance
|
||||||
|
const app = createApp({ connection })
|
||||||
|
```
|
||||||
|
|
||||||
|
## Initial Structure
|
||||||
|
To provide an initial database structure, you can pass `initialConfig` to the creation of an app. This will only be used if there isn't an existing configuration found in the database given. Here is a quick example:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { em, entity, text, number } from "bknd/data";
|
||||||
|
|
||||||
|
const schema = em({
|
||||||
|
posts: entity("posts", {
|
||||||
|
// "id" is automatically added
|
||||||
|
title: text().required(),
|
||||||
|
slug: text().required(),
|
||||||
|
content: text(),
|
||||||
|
views: number()
|
||||||
|
}),
|
||||||
|
comments: entity("comments", {
|
||||||
|
content: text()
|
||||||
|
})
|
||||||
|
|
||||||
|
// relations and indices are defined separately.
|
||||||
|
// the first argument are the helper functions, the second the entities.
|
||||||
|
}, ({ relation, index }, { posts, comments }) => {
|
||||||
|
relation(comments).manyToOne(posts);
|
||||||
|
// relation as well as index can be chained!
|
||||||
|
index(posts).on(["title"]).on(["slug"], true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// to get a type from your schema, use:
|
||||||
|
type Database = (typeof schema)["DB"];
|
||||||
|
// type Database = {
|
||||||
|
// posts: {
|
||||||
|
// id: number;
|
||||||
|
// title: string;
|
||||||
|
// content: string;
|
||||||
|
// views: number;
|
||||||
|
// },
|
||||||
|
// comments: {
|
||||||
|
// id: number;
|
||||||
|
// content: string;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pass the schema to the app
|
||||||
|
const app = createApp({
|
||||||
|
connection: { /* ... */ },
|
||||||
|
initialConfig: {
|
||||||
|
data: schema.toJSON()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that we didn't add relational fields directly to the entity, but instead defined them afterwards. That is because the relations are managed outside the entity scope to have an unified expierence for all kinds of relations (e.g. many-to-many).
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Defined relations are currently not part of the produced types for the structure. We're working on that, but in the meantime, you can define them manually.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
### Type completion
|
||||||
|
All entity related functions use the types defined in `DB` from `bknd/core`. To get type completion, you can extend that interface with your own schema:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { em } from "bknd/data";
|
||||||
|
import { Api } from "bknd";
|
||||||
|
|
||||||
|
// const schema = em({ ... });
|
||||||
|
|
||||||
|
type Database = (typeof schema)["DB"];
|
||||||
|
declare module "bknd/core" {
|
||||||
|
interface DB extends Database {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = new Api({ /* ... */ });
|
||||||
|
const { data: posts } = await api.data.readMany("posts", {})
|
||||||
|
// `posts` is now typed as Database["posts"]
|
||||||
|
```
|
||||||
|
|
||||||
|
The type completion is available for the API as well as all provided [React hooks](/react).
|
||||||
|
|
||||||
|
### Seeding the database
|
||||||
|
To seed your database with initial data, you can pass a `seed` function to the configuration. It
|
||||||
|
provides the `ModuleBuildContext` ([reference](/setup/introduction#modulebuildcontext)) as the first argument.
|
||||||
|
|
||||||
|
Note that the seed function will only be executed on app's first boot. If a configuration
|
||||||
|
already exists in the database, it will not be executed.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { createApp, type ModuleBuildContext } from "bknd";
|
||||||
|
|
||||||
|
const app = createApp({
|
||||||
|
connection: { /* ... */ },
|
||||||
|
initialConfig: { /* ... */ },
|
||||||
|
options: {
|
||||||
|
seed: async (ctx: ModuleBuildContext) => {
|
||||||
|
await ctx.em.mutator("posts").insertMany([
|
||||||
|
{ title: "First post", slug: "first-post", content: "..." },
|
||||||
|
{ title: "Second post", slug: "second-post" }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
205
docs/setup/introduction.mdx
Normal file
205
docs/setup/introduction.mdx
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
---
|
||||||
|
title: 'Introduction'
|
||||||
|
description: 'Setting up bknd'
|
||||||
|
---
|
||||||
|
|
||||||
|
There are several methods to get **bknd** up and running. You can choose between these options:
|
||||||
|
1. [Run it using the CLI](/cli): That's the easiest and fastest way to get started.
|
||||||
|
2. Use a runtime like [Node](/integration/node), [Bun](/integration/bun) or
|
||||||
|
[Cloudflare](/integration/cloudflare) (workerd). This will run the API and UI in the runtime's
|
||||||
|
native server and serves the UI assets statically from `node_modules`.
|
||||||
|
3. Run it inside your React framework of choice like [Next.js](/integration/nextjs),
|
||||||
|
[Astro](/integration/astro) or [Remix](/integration/remix).
|
||||||
|
|
||||||
|
There is also a fourth option, which is running it inside a
|
||||||
|
[Docker container](/integration/docker). This is essentially a wrapper around the CLI.
|
||||||
|
|
||||||
|
## Basic setup
|
||||||
|
Regardless of the method you choose, at the end all adapters come down to the actual
|
||||||
|
instantiation of the `App`, which in raw looks like this:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { createApp, type CreateAppConfig } from "bknd";
|
||||||
|
|
||||||
|
// create the app
|
||||||
|
const config = { /* ... */ } satisfies CreateAppConfig;
|
||||||
|
const app = createApp(config);
|
||||||
|
|
||||||
|
// build the app
|
||||||
|
await app.build();
|
||||||
|
|
||||||
|
// export for Web API compliant envs
|
||||||
|
export default app;
|
||||||
|
```
|
||||||
|
|
||||||
|
In Web API compliant environments, all you have to do is to default exporting the app, as it
|
||||||
|
implements the `Fetch` API.
|
||||||
|
|
||||||
|
## Configuration (`CreateAppConfig`)
|
||||||
|
The `CreateAppConfig` type is the main configuration object for the `createApp` function. It has
|
||||||
|
the following properties:
|
||||||
|
```ts
|
||||||
|
import type { Connection } from "bknd/data";
|
||||||
|
import type { Config } from "@libsql/client";
|
||||||
|
|
||||||
|
type AppPlugin = (app: App) => Promise<void> | void;
|
||||||
|
type LibSqlCredentials = Config;
|
||||||
|
|
||||||
|
type CreateAppConfig = {
|
||||||
|
connection?:
|
||||||
|
| Connection
|
||||||
|
| {
|
||||||
|
type: "libsql";
|
||||||
|
config: LibSqlCredentials;
|
||||||
|
};
|
||||||
|
initialConfig?: InitialModuleConfigs;
|
||||||
|
plugins?: AppPlugin[];
|
||||||
|
options?: {
|
||||||
|
basePath?: string;
|
||||||
|
trustFetched?: boolean;
|
||||||
|
onFirstBoot?: () => Promise<void>;
|
||||||
|
seed?: (ctx: ModuleBuildContext) => Promise<void>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
### `connection`
|
||||||
|
The `connection` property is the main connection object to the database. It can be either an
|
||||||
|
object with a type specifier (only `libsql` is supported at the moment) and the actual
|
||||||
|
`Connection` class. The `libsql` connection object looks like this:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const connection = {
|
||||||
|
type: "libsql",
|
||||||
|
config: {
|
||||||
|
url: string;
|
||||||
|
authToken?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, you can pass an instance of a `Connection` class directly,
|
||||||
|
see [Custom Connection](/setup/database#custom-connection) as a reference.
|
||||||
|
|
||||||
|
If the connection object is omitted, the app will try to use an in-memory database.
|
||||||
|
|
||||||
|
### `initialConfig`
|
||||||
|
As initial configuration, you can either pass a partial configuration object or a complete one
|
||||||
|
with a version number. The version number is used to automatically migrate the configuration up
|
||||||
|
to the latest version upon boot. The default configuration looks like this:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"server": {
|
||||||
|
"admin": {
|
||||||
|
"basepath": "",
|
||||||
|
"color_scheme": "light",
|
||||||
|
"logo_return_path": "/"
|
||||||
|
},
|
||||||
|
"cors": {
|
||||||
|
"origin": "*",
|
||||||
|
"allow_methods": ["GET", "POST", "PATCH", "PUT", "DELETE" ],
|
||||||
|
"allow_headers": ["Content-Type", "Content-Length", "Authorization", "Accept"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"basepath": "/api/data",
|
||||||
|
"entities": {},
|
||||||
|
"relations": {},
|
||||||
|
"indices": {}
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"enabled": false,
|
||||||
|
"basepath": "/api/auth",
|
||||||
|
"entity_name": "users",
|
||||||
|
"allow_register": true,
|
||||||
|
"jwt": {
|
||||||
|
"secret": "",
|
||||||
|
"alg": "HS256",
|
||||||
|
"fields": ["id", "email", "role"]
|
||||||
|
},
|
||||||
|
"cookie": {
|
||||||
|
"path": "/",
|
||||||
|
"sameSite": "lax",
|
||||||
|
"secure": true,
|
||||||
|
"httpOnly": true,
|
||||||
|
"expires": 604800,
|
||||||
|
"renew": true,
|
||||||
|
"pathSuccess": "/",
|
||||||
|
"pathLoggedOut": "/"
|
||||||
|
},
|
||||||
|
"strategies": {
|
||||||
|
"password": {
|
||||||
|
"type": "password",
|
||||||
|
"config": {
|
||||||
|
"hashing": "sha256"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"roles": {}
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"enabled": false,
|
||||||
|
"basepath": "/api/media",
|
||||||
|
"entity_name": "media",
|
||||||
|
"storage": {}
|
||||||
|
},
|
||||||
|
"flows": {
|
||||||
|
"basepath": "/api/flows",
|
||||||
|
"flows": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can use the CLI to get the default configuration:
|
||||||
|
```sh
|
||||||
|
npx bknd config --pretty
|
||||||
|
```
|
||||||
|
|
||||||
|
To validate your configuration against a JSON schema, you can also dump the schema using the CLI:
|
||||||
|
```sh
|
||||||
|
npx bknd schema
|
||||||
|
```
|
||||||
|
|
||||||
|
To create an initial data structure, you can use helpers [described here](/setup/database#initial-structure).
|
||||||
|
|
||||||
|
### `plugins`
|
||||||
|
The `plugins` property is an array of functions that are called after the app has been built,
|
||||||
|
but before its event is emitted. This is useful for adding custom routes or other functionality.
|
||||||
|
A simple plugin that adds a custom route looks like this:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const myPlugin: AppPlugin = (app) => {
|
||||||
|
app.server.get("/hello", (c) => c.json({ hello: "world" }));
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Since each plugin has full access to the `app` instance, it can add routes, modify the database
|
||||||
|
structure, add custom middlewares, respond to or add events, etc. Plugins are very powerful, so
|
||||||
|
make sure to only run trusted ones.
|
||||||
|
|
||||||
|
### `options`
|
||||||
|
This object is passed to the `ModuleManager` which is responsible for:
|
||||||
|
- validating and maintaining configuration of all modules
|
||||||
|
- building all modules (data, auth, media, flows)
|
||||||
|
- maintaining the `ModuleBuildContext` used by the modules
|
||||||
|
|
||||||
|
The `options` object has the following properties:
|
||||||
|
- `basePath` (`string`): The base path for the Hono instance. This is used to prefix all routes.
|
||||||
|
- `trustFetched` (`boolean`): If set to `true`, the app will not perform any validity checks for
|
||||||
|
the given or fetched configuration.
|
||||||
|
- `onFirstBoot` (`() => Promise<void>`): A function that is called when the app is booted for
|
||||||
|
the first time.
|
||||||
|
- `seed` (`(ctx: ModuleBuildContext) => Promise<void>`): A function that is called when the app is
|
||||||
|
booted for the first time and an initial partial configuration is provided.
|
||||||
|
|
||||||
|
|
||||||
|
## `ModuleBuildContext`
|
||||||
|
```ts
|
||||||
|
type ModuleBuildContext = {
|
||||||
|
connection: Connection;
|
||||||
|
server: Hono;
|
||||||
|
em: EntityManager;
|
||||||
|
emgr: EventManager;
|
||||||
|
guard: Guard;
|
||||||
|
};
|
||||||
|
```
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"_variables": {
|
"_variables": {
|
||||||
"lastUpdateCheck": 1732785435939
|
"lastUpdateCheck": 1734966049246
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1
examples/astro/.gitignore
vendored
1
examples/astro/.gitignore
vendored
@@ -22,3 +22,4 @@ pnpm-debug.log*
|
|||||||
|
|
||||||
# jetbrains setting folder
|
# jetbrains setting folder
|
||||||
.idea/
|
.idea/
|
||||||
|
*.db
|
||||||
@@ -14,7 +14,7 @@ export const prerender = false;
|
|||||||
<body>
|
<body>
|
||||||
<Admin
|
<Admin
|
||||||
withProvider={{ user }}
|
withProvider={{ user }}
|
||||||
config={{ basepath: "/admin", color_scheme: "dark" }}
|
config={{ basepath: "/admin", color_scheme: "dark", logo_return_path: "/../" }}
|
||||||
client:only
|
client:only
|
||||||
/>
|
/>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user