mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
removed admin config from server, theme is now client side, fixed module manager migrations
This commit is contained in:
@@ -4,6 +4,7 @@ import { AuthController } from "../../src/auth/api/AuthController";
|
|||||||
import { em, entity, make, text } from "../../src/data";
|
import { em, entity, make, text } from "../../src/data";
|
||||||
import { AppAuth, type ModuleBuildContext } from "../../src/modules";
|
import { AppAuth, type ModuleBuildContext } from "../../src/modules";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "../helper";
|
import { disableConsoleLog, enableConsoleLog } from "../helper";
|
||||||
|
// @ts-ignore
|
||||||
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
||||||
|
|
||||||
describe("AppAuth", () => {
|
describe("AppAuth", () => {
|
||||||
@@ -22,7 +23,7 @@ describe("AppAuth", () => {
|
|||||||
|
|
||||||
const config = auth.toJSON();
|
const config = auth.toJSON();
|
||||||
expect(config.jwt).toBeUndefined();
|
expect(config.jwt).toBeUndefined();
|
||||||
expect(config.strategies.password.config).toBeUndefined();
|
expect(config.strategies?.password?.config).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("enabling auth: generate secret", async () => {
|
test("enabling auth: generate secret", async () => {
|
||||||
@@ -42,6 +43,7 @@ describe("AppAuth", () => {
|
|||||||
const auth = new AppAuth(
|
const auth = new AppAuth(
|
||||||
{
|
{
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
// @ts-ignore
|
||||||
jwt: {
|
jwt: {
|
||||||
secret: "123456",
|
secret: "123456",
|
||||||
},
|
},
|
||||||
@@ -75,7 +77,7 @@ describe("AppAuth", () => {
|
|||||||
|
|
||||||
const { data: users } = await ctx.em.repository("users").findMany();
|
const { data: users } = await ctx.em.repository("users").findMany();
|
||||||
expect(users.length).toBe(1);
|
expect(users.length).toBe(1);
|
||||||
expect(users[0].email).toBe("some@body.com");
|
expect(users[0]?.email).toBe("some@body.com");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ describe("ModuleManager", async () => {
|
|||||||
}).toJSON(),
|
}).toJSON(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
}) as any;
|
||||||
//const { version, ...json } = mm.toJSON() as any;
|
//const { version, ...json } = mm.toJSON() as any;
|
||||||
|
|
||||||
const c2 = getDummyConnection();
|
const c2 = getDummyConnection();
|
||||||
@@ -73,7 +73,7 @@ describe("ModuleManager", async () => {
|
|||||||
}).toJSON(),
|
}).toJSON(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
} as any;
|
||||||
//const { version, ...json } = mm.toJSON() as any;
|
//const { version, ...json } = mm.toJSON() as any;
|
||||||
|
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
@@ -90,23 +90,20 @@ describe("ModuleManager", async () => {
|
|||||||
await mm2.build();
|
await mm2.build();
|
||||||
|
|
||||||
expect(stripMark(json)).toEqual(stripMark(mm2.configs()));
|
expect(stripMark(json)).toEqual(stripMark(mm2.configs()));
|
||||||
expect(mm2.configs().data.entities.test).toBeDefined();
|
expect(mm2.configs().data.entities?.test).toBeDefined();
|
||||||
expect(mm2.configs().data.entities.test.fields.content).toBeDefined();
|
expect(mm2.configs().data.entities?.test?.fields?.content).toBeDefined();
|
||||||
expect(mm2.get("data").toJSON().entities.test.fields.content).toBeDefined();
|
expect(mm2.get("data").toJSON().entities?.test?.fields?.content).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("s4: config given, table exists, version outdated, migrate", async () => {
|
test("s4: config given, table exists, version outdated, migrate", async () => {
|
||||||
const c = getDummyConnection();
|
const c = getDummyConnection();
|
||||||
const mm = new ModuleManager(c.dummyConnection);
|
const mm = new ModuleManager(c.dummyConnection);
|
||||||
await mm.build();
|
await mm.build();
|
||||||
const version = mm.version();
|
|
||||||
const json = mm.configs();
|
const json = mm.configs();
|
||||||
|
|
||||||
const c2 = getDummyConnection();
|
const c2 = getDummyConnection();
|
||||||
const db = c2.dummyConnection.kysely;
|
const db = c2.dummyConnection.kysely;
|
||||||
const mm2 = new ModuleManager(c2.dummyConnection, {
|
const mm2 = new ModuleManager(c2.dummyConnection);
|
||||||
initial: { version: version - 1, ...json },
|
|
||||||
});
|
|
||||||
await mm2.syncConfigTable();
|
await mm2.syncConfigTable();
|
||||||
|
|
||||||
await db
|
await db
|
||||||
@@ -171,7 +168,7 @@ describe("ModuleManager", async () => {
|
|||||||
expect(mm2.configs().data.basepath).toBe("/api/data2");
|
expect(mm2.configs().data.basepath).toBe("/api/data2");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("blank app, modify config", async () => {
|
/*test("blank app, modify config", async () => {
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
|
|
||||||
const mm = new ModuleManager(dummyConnection);
|
const mm = new ModuleManager(dummyConnection);
|
||||||
@@ -194,7 +191,7 @@ describe("ModuleManager", async () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});*/
|
||||||
|
|
||||||
test("partial config given", async () => {
|
test("partial config given", async () => {
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
@@ -212,13 +209,15 @@ describe("ModuleManager", async () => {
|
|||||||
expect(mm.version()).toBe(CURRENT_VERSION);
|
expect(mm.version()).toBe(CURRENT_VERSION);
|
||||||
expect(mm.built()).toBe(true);
|
expect(mm.built()).toBe(true);
|
||||||
expect(mm.configs().auth.enabled).toBe(true);
|
expect(mm.configs().auth.enabled).toBe(true);
|
||||||
expect(mm.configs().data.entities.users).toBeDefined();
|
expect(mm.configs().data.entities?.users).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("partial config given, but db version exists", async () => {
|
test("partial config given, but db version exists", async () => {
|
||||||
const c = getDummyConnection();
|
const c = getDummyConnection();
|
||||||
const mm = new ModuleManager(c.dummyConnection);
|
const mm = new ModuleManager(c.dummyConnection);
|
||||||
await mm.build();
|
await mm.build();
|
||||||
|
console.log("==".repeat(30));
|
||||||
|
console.log("");
|
||||||
const json = mm.configs();
|
const json = mm.configs();
|
||||||
|
|
||||||
const c2 = getDummyConnection();
|
const c2 = getDummyConnection();
|
||||||
@@ -265,8 +264,8 @@ describe("ModuleManager", async () => {
|
|||||||
|
|
||||||
override async build() {
|
override async build() {
|
||||||
//console.log("building FailingModule", this.config);
|
//console.log("building FailingModule", this.config);
|
||||||
if (this.config.value < 0) {
|
if (this.config.value && this.config.value < 0) {
|
||||||
throw new Error("value must be positive");
|
throw new Error("value must be positive, given: " + this.config.value);
|
||||||
}
|
}
|
||||||
this.setBuilt();
|
this.setBuilt();
|
||||||
}
|
}
|
||||||
@@ -332,6 +331,7 @@ describe("ModuleManager", async () => {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const f = mm.mutateConfigSafe("failing");
|
const f = mm.mutateConfigSafe("failing");
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
expect(f.set({ value: 2 })).resolves.toBeDefined();
|
expect(f.set({ value: 2 })).resolves.toBeDefined();
|
||||||
expect(mockOnUpdated).toHaveBeenCalled();
|
expect(mockOnUpdated).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,39 +1,44 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { type InitialModuleConfigs, createApp } from "../../../src";
|
import { type InitialModuleConfigs, createApp } from "../../../src";
|
||||||
|
|
||||||
import type { Kysely } from "kysely";
|
import { type Kysely, sql } from "kysely";
|
||||||
import { getDummyConnection } from "../../helper";
|
import { getDummyConnection } from "../../helper";
|
||||||
import v7 from "./samples/v7.json";
|
import v7 from "./samples/v7.json";
|
||||||
|
import v8 from "./samples/v8.json";
|
||||||
|
import v8_2 from "./samples/v8-2.json";
|
||||||
|
|
||||||
// app expects migratable config to be present in database
|
// app expects migratable config to be present in database
|
||||||
async function createVersionedApp(config: InitialModuleConfigs) {
|
async function createVersionedApp(config: InitialModuleConfigs | any) {
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
|
|
||||||
if (!("version" in config)) throw new Error("config must have a version");
|
if (!("version" in config)) throw new Error("config must have a version");
|
||||||
const { version, ...rest } = config;
|
const { version, ...rest } = config;
|
||||||
|
|
||||||
const app = createApp({ connection: dummyConnection });
|
const db = dummyConnection.kysely as Kysely<any>;
|
||||||
await app.build();
|
await sql`CREATE TABLE "__bknd" (
|
||||||
|
"id" integer not null primary key autoincrement,
|
||||||
|
"version" integer,
|
||||||
|
"type" text,
|
||||||
|
"json" text,
|
||||||
|
"created_at" datetime,
|
||||||
|
"updated_at" datetime
|
||||||
|
)`.execute(db);
|
||||||
|
|
||||||
const qb = app.modules.ctx().connection.kysely as Kysely<any>;
|
await db
|
||||||
const current = await qb
|
.insertInto("__bknd")
|
||||||
.selectFrom("__bknd")
|
.values({
|
||||||
.selectAll()
|
version,
|
||||||
.where("type", "=", "config")
|
type: "config",
|
||||||
.executeTakeFirst();
|
created_at: new Date().toISOString(),
|
||||||
|
json: JSON.stringify(rest),
|
||||||
await qb
|
})
|
||||||
.updateTable("__bknd")
|
|
||||||
.set("json", JSON.stringify(rest))
|
|
||||||
.set("version", 7)
|
|
||||||
.where("id", "=", current!.id)
|
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
const app2 = createApp({
|
const app = createApp({
|
||||||
connection: dummyConnection,
|
connection: dummyConnection,
|
||||||
});
|
});
|
||||||
await app2.build();
|
await app.build();
|
||||||
return app2;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Migrations", () => {
|
describe("Migrations", () => {
|
||||||
@@ -46,8 +51,8 @@ describe("Migrations", () => {
|
|||||||
|
|
||||||
const app = await createVersionedApp(v7);
|
const app = await createVersionedApp(v7);
|
||||||
|
|
||||||
expect(app.version()).toBe(8);
|
expect(app.version()).toBeGreaterThan(7);
|
||||||
expect(app.toJSON(true).auth.strategies.password.enabled).toBe(true);
|
expect(app.toJSON(true).auth.strategies?.password?.enabled).toBe(true);
|
||||||
|
|
||||||
const req = await app.server.request("/api/auth/password/register", {
|
const req = await app.server.request("/api/auth/password/register", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -60,7 +65,30 @@ describe("Migrations", () => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
expect(req.ok).toBe(true);
|
expect(req.ok).toBe(true);
|
||||||
const res = await req.json();
|
const res = (await req.json()) as any;
|
||||||
expect(res.user.email).toBe("test@test.com");
|
expect(res.user.email).toBe("test@test.com");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("migration from 8 to 9", async () => {
|
||||||
|
expect(v8.version).toBe(8);
|
||||||
|
|
||||||
|
const app = await createVersionedApp(v8);
|
||||||
|
|
||||||
|
expect(app.version()).toBeGreaterThan(8);
|
||||||
|
// @ts-expect-error
|
||||||
|
expect(app.toJSON(true).server.admin).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.only("migration from 8 to 9 (from initial)", async () => {
|
||||||
|
expect(v8_2.version).toBe(8);
|
||||||
|
|
||||||
|
const app = createApp({
|
||||||
|
connection: getDummyConnection().dummyConnection,
|
||||||
|
initialConfig: v8_2 as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(app.version()).toBeGreaterThan(8);
|
||||||
|
// @ts-expect-error
|
||||||
|
expect(app.toJSON(true).server.admin).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
709
app/__test__/modules/migrations/samples/v8-2.json
Normal file
709
app/__test__/modules/migrations/samples/v8-2.json
Normal file
@@ -0,0 +1,709 @@
|
|||||||
|
{
|
||||||
|
"version": 8,
|
||||||
|
"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": {
|
||||||
|
"products": {
|
||||||
|
"type": "regular",
|
||||||
|
"fields": {
|
||||||
|
"id": {
|
||||||
|
"type": "primary",
|
||||||
|
"config": {
|
||||||
|
"fillable": false,
|
||||||
|
"required": false,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": true,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"brand": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": true,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"currency": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"price": {
|
||||||
|
"type": "number",
|
||||||
|
"config": {
|
||||||
|
"required": true,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"price_compare": {
|
||||||
|
"type": "number",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": ["table"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"html_config": {
|
||||||
|
"element": "input"
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": ["table"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "date",
|
||||||
|
"config": {
|
||||||
|
"type": "date",
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": ["table"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"html_config": {
|
||||||
|
"element": "textarea",
|
||||||
|
"props": {
|
||||||
|
"rows": 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": ["table"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"images": {
|
||||||
|
"type": "media",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": ["update"],
|
||||||
|
"hidden": false,
|
||||||
|
"mime_types": [],
|
||||||
|
"virtual": true,
|
||||||
|
"entity": "products"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"identifier": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"type": "jsonschema",
|
||||||
|
"config": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"size": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"gender": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"ai_description": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": ["string", "number"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": ["table"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_likes": {
|
||||||
|
"type": "number",
|
||||||
|
"config": {
|
||||||
|
"default_value": 0,
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"sort_field": "id",
|
||||||
|
"sort_dir": "desc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"type": "system",
|
||||||
|
"fields": {
|
||||||
|
"id": {
|
||||||
|
"type": "primary",
|
||||||
|
"config": {
|
||||||
|
"fillable": false,
|
||||||
|
"required": false,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": true,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"folder": {
|
||||||
|
"type": "boolean",
|
||||||
|
"config": {
|
||||||
|
"default_value": false,
|
||||||
|
"hidden": true,
|
||||||
|
"fillable": ["create"],
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mime_type": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"type": "number",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"hidden": true,
|
||||||
|
"fillable": ["create"],
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"etag": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modified_at": {
|
||||||
|
"type": "date",
|
||||||
|
"config": {
|
||||||
|
"type": "datetime",
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reference": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entity_id": {
|
||||||
|
"type": "number",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"type": "json",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"temporary": {
|
||||||
|
"type": "boolean",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"sort_field": "id",
|
||||||
|
"sort_dir": "desc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"type": "system",
|
||||||
|
"fields": {
|
||||||
|
"id": {
|
||||||
|
"type": "primary",
|
||||||
|
"config": {
|
||||||
|
"fillable": false,
|
||||||
|
"required": false,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": true,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"strategy": {
|
||||||
|
"type": "enum",
|
||||||
|
"config": {
|
||||||
|
"options": {
|
||||||
|
"type": "strings",
|
||||||
|
"values": ["password"]
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"fillable": ["create"],
|
||||||
|
"hidden": ["read", "table", "update", "form"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"strategy_value": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"fillable": ["create"],
|
||||||
|
"hidden": ["read", "table", "update", "form"],
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"type": "enum",
|
||||||
|
"config": {
|
||||||
|
"options": {
|
||||||
|
"type": "strings",
|
||||||
|
"values": ["guest", "admin"]
|
||||||
|
},
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_boards": {
|
||||||
|
"type": "number",
|
||||||
|
"config": {
|
||||||
|
"default_value": 0,
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"sort_field": "id",
|
||||||
|
"sort_dir": "desc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"product_likes": {
|
||||||
|
"type": "regular",
|
||||||
|
"fields": {
|
||||||
|
"id": {
|
||||||
|
"type": "primary",
|
||||||
|
"config": {
|
||||||
|
"fillable": false,
|
||||||
|
"required": false,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "date",
|
||||||
|
"config": {
|
||||||
|
"type": "datetime",
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"users_id": {
|
||||||
|
"type": "relation",
|
||||||
|
"config": {
|
||||||
|
"label": "User",
|
||||||
|
"required": true,
|
||||||
|
"reference": "users",
|
||||||
|
"target": "users",
|
||||||
|
"target_field": "id",
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false,
|
||||||
|
"on_delete": "set null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"products_id": {
|
||||||
|
"type": "relation",
|
||||||
|
"config": {
|
||||||
|
"label": "Product",
|
||||||
|
"required": true,
|
||||||
|
"reference": "products",
|
||||||
|
"target": "products",
|
||||||
|
"target_field": "id",
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false,
|
||||||
|
"on_delete": "set null"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"sort_field": "id",
|
||||||
|
"sort_dir": "desc",
|
||||||
|
"name": "Product Likes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"boards": {
|
||||||
|
"type": "regular",
|
||||||
|
"fields": {
|
||||||
|
"id": {
|
||||||
|
"type": "primary",
|
||||||
|
"config": {
|
||||||
|
"fillable": false,
|
||||||
|
"required": false,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"private": {
|
||||||
|
"type": "boolean",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"users_id": {
|
||||||
|
"type": "relation",
|
||||||
|
"config": {
|
||||||
|
"label": "Users",
|
||||||
|
"required": true,
|
||||||
|
"reference": "users",
|
||||||
|
"target": "users",
|
||||||
|
"target_field": "id",
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false,
|
||||||
|
"on_delete": "set null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"images": {
|
||||||
|
"type": "media",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": ["update"],
|
||||||
|
"hidden": false,
|
||||||
|
"mime_types": [],
|
||||||
|
"virtual": true,
|
||||||
|
"entity": "boards",
|
||||||
|
"max_items": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cover": {
|
||||||
|
"type": "number",
|
||||||
|
"config": {
|
||||||
|
"default_value": 0,
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_products": {
|
||||||
|
"type": "number",
|
||||||
|
"config": {
|
||||||
|
"default_value": 0,
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"sort_field": "id",
|
||||||
|
"sort_dir": "desc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"boards_products": {
|
||||||
|
"type": "generated",
|
||||||
|
"fields": {
|
||||||
|
"id": {
|
||||||
|
"type": "primary",
|
||||||
|
"config": {
|
||||||
|
"fillable": false,
|
||||||
|
"required": false,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"boards_id": {
|
||||||
|
"type": "relation",
|
||||||
|
"config": {
|
||||||
|
"required": true,
|
||||||
|
"reference": "boards",
|
||||||
|
"target": "boards",
|
||||||
|
"target_field": "id",
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false,
|
||||||
|
"on_delete": "set null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"products_id": {
|
||||||
|
"type": "relation",
|
||||||
|
"config": {
|
||||||
|
"required": true,
|
||||||
|
"reference": "products",
|
||||||
|
"target": "products",
|
||||||
|
"target_field": "id",
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false,
|
||||||
|
"on_delete": "set null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"manual": {
|
||||||
|
"type": "boolean",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"sort_field": "id",
|
||||||
|
"sort_dir": "desc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"relations": {
|
||||||
|
"poly_products_media_images": {
|
||||||
|
"type": "poly",
|
||||||
|
"source": "products",
|
||||||
|
"target": "media",
|
||||||
|
"config": {
|
||||||
|
"mappedBy": "images"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"n1_product_likes_users": {
|
||||||
|
"type": "n:1",
|
||||||
|
"source": "product_likes",
|
||||||
|
"target": "users",
|
||||||
|
"config": {
|
||||||
|
"mappedBy": "",
|
||||||
|
"inversedBy": "",
|
||||||
|
"required": true,
|
||||||
|
"with_limit": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"n1_product_likes_products": {
|
||||||
|
"type": "n:1",
|
||||||
|
"source": "product_likes",
|
||||||
|
"target": "products",
|
||||||
|
"config": {
|
||||||
|
"mappedBy": "",
|
||||||
|
"inversedBy": "",
|
||||||
|
"required": true,
|
||||||
|
"with_limit": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"n1_boards_users": {
|
||||||
|
"type": "n:1",
|
||||||
|
"source": "boards",
|
||||||
|
"target": "users",
|
||||||
|
"config": {
|
||||||
|
"mappedBy": "",
|
||||||
|
"inversedBy": "",
|
||||||
|
"required": true,
|
||||||
|
"with_limit": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"poly_boards_media_images": {
|
||||||
|
"type": "poly",
|
||||||
|
"source": "boards",
|
||||||
|
"target": "media",
|
||||||
|
"config": {
|
||||||
|
"mappedBy": "images",
|
||||||
|
"targetCardinality": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mn_boards_products_boards_products,boards_products": {
|
||||||
|
"type": "m:n",
|
||||||
|
"source": "boards",
|
||||||
|
"target": "products",
|
||||||
|
"config": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indices": {
|
||||||
|
"idx_unique_media_path": {
|
||||||
|
"entity": "media",
|
||||||
|
"fields": ["path"],
|
||||||
|
"unique": true
|
||||||
|
},
|
||||||
|
"idx_media_reference": {
|
||||||
|
"entity": "media",
|
||||||
|
"fields": ["reference"],
|
||||||
|
"unique": false
|
||||||
|
},
|
||||||
|
"idx_unique_users_email": {
|
||||||
|
"entity": "users",
|
||||||
|
"fields": ["email"],
|
||||||
|
"unique": true
|
||||||
|
},
|
||||||
|
"idx_users_strategy": {
|
||||||
|
"entity": "users",
|
||||||
|
"fields": ["strategy"],
|
||||||
|
"unique": false
|
||||||
|
},
|
||||||
|
"idx_users_strategy_value": {
|
||||||
|
"entity": "users",
|
||||||
|
"fields": ["strategy_value"],
|
||||||
|
"unique": false
|
||||||
|
},
|
||||||
|
"idx_product_likes_unique_products_id_users_id": {
|
||||||
|
"entity": "product_likes",
|
||||||
|
"fields": ["products_id", "users_id"],
|
||||||
|
"unique": true
|
||||||
|
},
|
||||||
|
"idx_boards_products_unique_boards_id_products_id": {
|
||||||
|
"entity": "boards_products",
|
||||||
|
"fields": ["boards_id", "products_id"],
|
||||||
|
"unique": true
|
||||||
|
},
|
||||||
|
"idx_media_entity_id": {
|
||||||
|
"entity": "media",
|
||||||
|
"fields": ["entity_id"],
|
||||||
|
"unique": false
|
||||||
|
},
|
||||||
|
"idx_products_identifier": {
|
||||||
|
"entity": "products",
|
||||||
|
"fields": ["identifier"],
|
||||||
|
"unique": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"enabled": true,
|
||||||
|
"basepath": "/api/auth",
|
||||||
|
"entity_name": "users",
|
||||||
|
"allow_register": true,
|
||||||
|
"jwt": {
|
||||||
|
"secret": "2wY76Z$JQg(3t?Wn8g,^ZqZhlmjNx<@uQjI!7i^XZBF11Xa1>zZK2??Y[D|]cc%k",
|
||||||
|
"alg": "HS256",
|
||||||
|
"fields": ["id", "email", "role"]
|
||||||
|
},
|
||||||
|
"cookie": {
|
||||||
|
"path": "/",
|
||||||
|
"sameSite": "lax",
|
||||||
|
"secure": true,
|
||||||
|
"httpOnly": true,
|
||||||
|
"expires": 604800,
|
||||||
|
"renew": true,
|
||||||
|
"pathSuccess": "/",
|
||||||
|
"pathLoggedOut": "/"
|
||||||
|
},
|
||||||
|
"strategies": {
|
||||||
|
"password": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "password",
|
||||||
|
"config": {
|
||||||
|
"hashing": "sha256"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"roles": {
|
||||||
|
"guest": {
|
||||||
|
"is_default": true,
|
||||||
|
"permissions": ["system.access.api", "data.entity.read"]
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"implicit_allow": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"guard": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"enabled": true,
|
||||||
|
"basepath": "/api/media",
|
||||||
|
"entity_name": "media",
|
||||||
|
"storage": {},
|
||||||
|
"adapter": {
|
||||||
|
"type": "s3",
|
||||||
|
"config": {
|
||||||
|
"access_key": "123",
|
||||||
|
"secret_access_key": "123",
|
||||||
|
"url": "https://123.r2.cloudflarestorage.com/enly"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flows": {
|
||||||
|
"basepath": "/api/flows",
|
||||||
|
"flows": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
598
app/__test__/modules/migrations/samples/v8.json
Normal file
598
app/__test__/modules/migrations/samples/v8.json
Normal file
@@ -0,0 +1,598 @@
|
|||||||
|
{
|
||||||
|
"version": 8,
|
||||||
|
"server": {
|
||||||
|
"admin": {
|
||||||
|
"basepath": "",
|
||||||
|
"logo_return_path": "/",
|
||||||
|
"color_scheme": "dark"
|
||||||
|
},
|
||||||
|
"cors": {
|
||||||
|
"origin": "*",
|
||||||
|
"allow_methods": ["GET", "POST", "PATCH", "PUT", "DELETE"],
|
||||||
|
"allow_headers": [
|
||||||
|
"Content-Type",
|
||||||
|
"Content-Length",
|
||||||
|
"Authorization",
|
||||||
|
"Accept"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"basepath": "/api/data",
|
||||||
|
"entities": {
|
||||||
|
"posts": {
|
||||||
|
"type": "regular",
|
||||||
|
"fields": {
|
||||||
|
"id": {
|
||||||
|
"type": "primary",
|
||||||
|
"config": {
|
||||||
|
"fillable": false,
|
||||||
|
"required": false,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": true,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"html_config": {
|
||||||
|
"element": "input"
|
||||||
|
},
|
||||||
|
"pattern": "^[a-z\\-\\_0-9]+$",
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false,
|
||||||
|
"label": "Slug"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"html_config": {
|
||||||
|
"element": "textarea",
|
||||||
|
"props": {
|
||||||
|
"rows": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": ["form"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": {
|
||||||
|
"type": "boolean",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"images": {
|
||||||
|
"type": "media",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": ["update"],
|
||||||
|
"hidden": false,
|
||||||
|
"mime_types": [],
|
||||||
|
"virtual": true,
|
||||||
|
"entity": "posts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"type": "jsonschema",
|
||||||
|
"config": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ui_schema": {
|
||||||
|
"ui:options": {
|
||||||
|
"orderable": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"users_id": {
|
||||||
|
"type": "relation",
|
||||||
|
"config": {
|
||||||
|
"label": "Users",
|
||||||
|
"required": false,
|
||||||
|
"reference": "users",
|
||||||
|
"target": "users",
|
||||||
|
"target_field": "id",
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false,
|
||||||
|
"on_delete": "set null"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"sort_field": "title",
|
||||||
|
"sort_dir": "desc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"comments": {
|
||||||
|
"type": "regular",
|
||||||
|
"fields": {
|
||||||
|
"id": {
|
||||||
|
"type": "primary",
|
||||||
|
"config": {
|
||||||
|
"fillable": false,
|
||||||
|
"required": false,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "date",
|
||||||
|
"config": {
|
||||||
|
"type": "date",
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"posts_id": {
|
||||||
|
"type": "relation",
|
||||||
|
"config": {
|
||||||
|
"label": "Posts",
|
||||||
|
"required": true,
|
||||||
|
"reference": "posts",
|
||||||
|
"target": "posts",
|
||||||
|
"target_field": "id",
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false,
|
||||||
|
"on_delete": "set null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"users_id": {
|
||||||
|
"type": "relation",
|
||||||
|
"config": {
|
||||||
|
"label": "Users",
|
||||||
|
"required": true,
|
||||||
|
"reference": "users",
|
||||||
|
"target": "users",
|
||||||
|
"target_field": "id",
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false,
|
||||||
|
"on_delete": "set null"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"sort_field": "id",
|
||||||
|
"sort_dir": "asc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"type": "system",
|
||||||
|
"fields": {
|
||||||
|
"id": {
|
||||||
|
"type": "primary",
|
||||||
|
"config": {
|
||||||
|
"fillable": false,
|
||||||
|
"required": false,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": true,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"folder": {
|
||||||
|
"type": "boolean",
|
||||||
|
"config": {
|
||||||
|
"default_value": false,
|
||||||
|
"hidden": true,
|
||||||
|
"fillable": ["create"],
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mime_type": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"type": "number",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"hidden": true,
|
||||||
|
"fillable": ["create"],
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"etag": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modified_at": {
|
||||||
|
"type": "date",
|
||||||
|
"config": {
|
||||||
|
"type": "datetime",
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reference": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entity_id": {
|
||||||
|
"type": "number",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"type": "json",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"sort_field": "id",
|
||||||
|
"sort_dir": "asc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"type": "system",
|
||||||
|
"fields": {
|
||||||
|
"id": {
|
||||||
|
"type": "primary",
|
||||||
|
"config": {
|
||||||
|
"fillable": false,
|
||||||
|
"required": false,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": true,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"strategy": {
|
||||||
|
"type": "enum",
|
||||||
|
"config": {
|
||||||
|
"options": {
|
||||||
|
"type": "strings",
|
||||||
|
"values": ["password", "google"]
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"fillable": ["create"],
|
||||||
|
"hidden": ["update", "form"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"strategy_value": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"fillable": ["create"],
|
||||||
|
"hidden": ["read", "table", "update", "form"],
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"type": "enum",
|
||||||
|
"config": {
|
||||||
|
"options": {
|
||||||
|
"type": "strings",
|
||||||
|
"values": ["guest", "admin", "editor"]
|
||||||
|
},
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"avatar": {
|
||||||
|
"type": "media",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": ["update"],
|
||||||
|
"hidden": false,
|
||||||
|
"mime_types": [],
|
||||||
|
"virtual": true,
|
||||||
|
"entity": "users",
|
||||||
|
"max_items": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"sort_field": "id",
|
||||||
|
"sort_dir": "asc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"type": "regular",
|
||||||
|
"fields": {
|
||||||
|
"id": {
|
||||||
|
"type": "primary",
|
||||||
|
"config": {
|
||||||
|
"fillable": false,
|
||||||
|
"required": false,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "text",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"number": {
|
||||||
|
"type": "number",
|
||||||
|
"config": {
|
||||||
|
"required": false,
|
||||||
|
"fillable": true,
|
||||||
|
"hidden": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"sort_field": "id",
|
||||||
|
"sort_dir": "asc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"relations": {
|
||||||
|
"n1_comments_posts": {
|
||||||
|
"type": "n:1",
|
||||||
|
"source": "comments",
|
||||||
|
"target": "posts",
|
||||||
|
"config": {
|
||||||
|
"required": true,
|
||||||
|
"with_limit": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"poly_posts_media_images": {
|
||||||
|
"type": "poly",
|
||||||
|
"source": "posts",
|
||||||
|
"target": "media",
|
||||||
|
"config": {
|
||||||
|
"mappedBy": "images"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"n1_posts_users": {
|
||||||
|
"type": "n:1",
|
||||||
|
"source": "posts",
|
||||||
|
"target": "users",
|
||||||
|
"config": {
|
||||||
|
"with_limit": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"n1_comments_users": {
|
||||||
|
"type": "n:1",
|
||||||
|
"source": "comments",
|
||||||
|
"target": "users",
|
||||||
|
"config": {
|
||||||
|
"required": true,
|
||||||
|
"with_limit": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"poly_users_media_avatar": {
|
||||||
|
"type": "poly",
|
||||||
|
"source": "users",
|
||||||
|
"target": "media",
|
||||||
|
"config": {
|
||||||
|
"mappedBy": "avatar",
|
||||||
|
"targetCardinality": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indices": {
|
||||||
|
"idx_unique_media_path": {
|
||||||
|
"entity": "media",
|
||||||
|
"fields": ["path"],
|
||||||
|
"unique": true
|
||||||
|
},
|
||||||
|
"idx_unique_users_email": {
|
||||||
|
"entity": "users",
|
||||||
|
"fields": ["email"],
|
||||||
|
"unique": true
|
||||||
|
},
|
||||||
|
"idx_users_strategy": {
|
||||||
|
"entity": "users",
|
||||||
|
"fields": ["strategy"],
|
||||||
|
"unique": false
|
||||||
|
},
|
||||||
|
"idx_users_strategy_value": {
|
||||||
|
"entity": "users",
|
||||||
|
"fields": ["strategy_value"],
|
||||||
|
"unique": false
|
||||||
|
},
|
||||||
|
"idx_media_reference": {
|
||||||
|
"entity": "media",
|
||||||
|
"fields": ["reference"],
|
||||||
|
"unique": false
|
||||||
|
},
|
||||||
|
"idx_media_entity_id": {
|
||||||
|
"entity": "media",
|
||||||
|
"fields": ["entity_id"],
|
||||||
|
"unique": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"enabled": true,
|
||||||
|
"basepath": "/api/auth",
|
||||||
|
"entity_name": "users",
|
||||||
|
"jwt": {
|
||||||
|
"secret": "A%3jk*wD!Zruj123123123j$Wm8qS8m8qS8",
|
||||||
|
"alg": "HS256",
|
||||||
|
"fields": ["id", "email", "role"],
|
||||||
|
"issuer": "showoff"
|
||||||
|
},
|
||||||
|
"guard": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"strategies": {
|
||||||
|
"password": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "password",
|
||||||
|
"config": {
|
||||||
|
"hashing": "sha256"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"google": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "oauth",
|
||||||
|
"config": {
|
||||||
|
"type": "oidc",
|
||||||
|
"client": {
|
||||||
|
"client_id": "545948917277-123ieuifrag.apps.googleusercontent.com",
|
||||||
|
"client_secret": "123-123hTTZfDDGPDPp"
|
||||||
|
},
|
||||||
|
"name": "google"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"roles": {
|
||||||
|
"guest": {
|
||||||
|
"permissions": [
|
||||||
|
"data.entity.read",
|
||||||
|
"system.access.api",
|
||||||
|
"system.config.read"
|
||||||
|
],
|
||||||
|
"is_default": true
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"is_default": false,
|
||||||
|
"implicit_allow": true
|
||||||
|
},
|
||||||
|
"editor": {
|
||||||
|
"permissions": [
|
||||||
|
"system.access.admin",
|
||||||
|
"system.config.read",
|
||||||
|
"system.schema.read",
|
||||||
|
"system.config.read.secrets",
|
||||||
|
"system.access.api",
|
||||||
|
"data.entity.read",
|
||||||
|
"data.entity.update",
|
||||||
|
"data.entity.delete",
|
||||||
|
"data.entity.create"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allow_register": true,
|
||||||
|
"cookie": {
|
||||||
|
"path": "/",
|
||||||
|
"sameSite": "lax",
|
||||||
|
"secure": true,
|
||||||
|
"httpOnly": true,
|
||||||
|
"expires": 604800,
|
||||||
|
"renew": true,
|
||||||
|
"pathSuccess": "/",
|
||||||
|
"pathLoggedOut": "/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"enabled": true,
|
||||||
|
"basepath": "/api/media",
|
||||||
|
"entity_name": "media",
|
||||||
|
"storage": {},
|
||||||
|
"adapter": {
|
||||||
|
"type": "s3",
|
||||||
|
"config": {
|
||||||
|
"access_key": "123",
|
||||||
|
"secret_access_key": "123",
|
||||||
|
"url": "https://123.r2.cloudflarestorage.com/bknd-123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flows": {
|
||||||
|
"basepath": "/api/flows",
|
||||||
|
"flows": {
|
||||||
|
"test": {
|
||||||
|
"trigger": {
|
||||||
|
"type": "http",
|
||||||
|
"config": {
|
||||||
|
"mode": "sync",
|
||||||
|
"method": "GET",
|
||||||
|
"response_type": "html",
|
||||||
|
"path": "/json-posts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tasks": {
|
||||||
|
"fetching": {
|
||||||
|
"type": "fetch",
|
||||||
|
"params": {
|
||||||
|
"method": "GET",
|
||||||
|
"headers": [],
|
||||||
|
"url": "https://jsonplaceholder.typicode.com/posts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"render": {
|
||||||
|
"type": "render",
|
||||||
|
"params": {
|
||||||
|
"render": "<h1>Posts</h1>\n<ul>\n {% for post in fetching.output %}\n <li>{{ post.title }}</li>\n {% endfor %}\n</ul>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"connections": {
|
||||||
|
"5cce66b5-57c6-4541-88ac-b298794c6c52": {
|
||||||
|
"source": "fetching",
|
||||||
|
"target": "render",
|
||||||
|
"config": {
|
||||||
|
"condition": {
|
||||||
|
"type": "success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"start_task": "fetching",
|
||||||
|
"responding_task": "render"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@ export class DebugLogger {
|
|||||||
|
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
const time = this.last === 0 ? 0 : 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(Math.max(this._context.length - 1, 0));
|
||||||
const context =
|
const context =
|
||||||
this._context.length > 0 ? `[${this._context[this._context.length - 1]}]` : "";
|
this._context.length > 0 ? `[${this._context[this._context.length - 1]}]` : "";
|
||||||
console.log(indents, context, time, ...args);
|
console.log(indents, context, time, ...args);
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ export class AppData extends Module<typeof dataConfigSchema> {
|
|||||||
indices: _indices = {},
|
indices: _indices = {},
|
||||||
} = this.config;
|
} = this.config;
|
||||||
|
|
||||||
this.ctx.logger.context("AppData").log("building with entities", Object.keys(_entities));
|
|
||||||
|
|
||||||
const entities = transformObject(_entities, (entityConfig, name) => {
|
const entities = transformObject(_entities, (entityConfig, name) => {
|
||||||
return constructEntity(name, entityConfig);
|
return constructEntity(name, entityConfig);
|
||||||
});
|
});
|
||||||
@@ -60,7 +58,6 @@ export class AppData extends Module<typeof dataConfigSchema> {
|
|||||||
);
|
);
|
||||||
this.ctx.guard.registerPermissions(Object.values(DataPermissions));
|
this.ctx.guard.registerPermissions(Object.values(DataPermissions));
|
||||||
|
|
||||||
this.ctx.logger.clear();
|
|
||||||
this.setBuilt();
|
this.setBuilt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ const configJsonSchema = Type.Union([
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
const __bknd = entity(TABLE_NAME, {
|
export const __bknd = entity(TABLE_NAME, {
|
||||||
version: number().required(),
|
version: number().required(),
|
||||||
type: enumm({ enum: ["config", "diff", "backup"] }).required(),
|
type: enumm({ enum: ["config", "diff", "backup"] }).required(),
|
||||||
json: jsonSchema({ schema: configJsonSchema }).required(),
|
json: jsonSchema({ schema: configJsonSchema }).required(),
|
||||||
@@ -170,6 +170,8 @@ export class ModuleManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.log("booted with", this._booted_with);
|
||||||
|
|
||||||
this.createModules(initial);
|
this.createModules(initial);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,7 +273,7 @@ export class ModuleManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetch(): Promise<ConfigTable> {
|
private async fetch(): Promise<ConfigTable | undefined> {
|
||||||
this.logger.context("fetch").log("fetching");
|
this.logger.context("fetch").log("fetching");
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
|
|
||||||
@@ -285,7 +287,7 @@ export class ModuleManager {
|
|||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
this.logger.log("error fetching").clear();
|
this.logger.log("error fetching").clear();
|
||||||
throw BkndError.with("no config");
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger
|
this.logger
|
||||||
@@ -305,6 +307,7 @@ export class ModuleManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const state = await this.fetch();
|
const state = await this.fetch();
|
||||||
|
if (!state) throw new BkndError("save: no config found");
|
||||||
this.logger.log("fetched version", state.version);
|
this.logger.log("fetched version", state.version);
|
||||||
|
|
||||||
if (state.version !== version) {
|
if (state.version !== version) {
|
||||||
@@ -321,11 +324,11 @@ export class ModuleManager {
|
|||||||
json: configs,
|
json: configs,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.logger.log("version matches");
|
this.logger.log("version matches", state.version);
|
||||||
|
|
||||||
// clean configs because of Diff() function
|
// clean configs because of Diff() function
|
||||||
const diffs = diff(state.json, clone(configs));
|
const diffs = diff(state.json, clone(configs));
|
||||||
this.logger.log("checking diff", diffs);
|
this.logger.log("checking diff", [diffs.length]);
|
||||||
|
|
||||||
if (diff.length > 0) {
|
if (diff.length > 0) {
|
||||||
// store diff
|
// store diff
|
||||||
@@ -380,78 +383,6 @@ export class ModuleManager {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async migrate() {
|
|
||||||
const state = {
|
|
||||||
success: false,
|
|
||||||
migrated: false,
|
|
||||||
version: {
|
|
||||||
before: this.version(),
|
|
||||||
after: this.version(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
this.logger.context("migrate").log("migrating?", this.version(), CURRENT_VERSION);
|
|
||||||
|
|
||||||
if (this.version() < CURRENT_VERSION) {
|
|
||||||
state.version.before = this.version();
|
|
||||||
|
|
||||||
this.logger.log("there are migrations, verify version");
|
|
||||||
// sync __bknd table
|
|
||||||
await this.syncConfigTable();
|
|
||||||
|
|
||||||
// modules must be built before migration
|
|
||||||
this.logger.log("building modules");
|
|
||||||
await this.buildModules({ graceful: true });
|
|
||||||
this.logger.log("modules built");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const state = await this.fetch();
|
|
||||||
if (state.version !== this.version()) {
|
|
||||||
// @todo: potentially drop provided config and use database version
|
|
||||||
throw new Error(
|
|
||||||
`Given version (${this.version()}) and fetched version (${state.version}) do not match.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
throw new Error(`Version is ${this.version()}, fetch failed: ${e.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log("now migrating");
|
|
||||||
let version = this.version();
|
|
||||||
let configs: any = this.configs();
|
|
||||||
//console.log("migrating with", version, configs);
|
|
||||||
if (Object.keys(configs).length === 0) {
|
|
||||||
throw new Error("No config to migrate");
|
|
||||||
}
|
|
||||||
|
|
||||||
const [_version, _configs] = await migrate(version, configs, {
|
|
||||||
db: this.db,
|
|
||||||
});
|
|
||||||
version = _version;
|
|
||||||
configs = _configs;
|
|
||||||
|
|
||||||
this._version = version;
|
|
||||||
state.version.after = version;
|
|
||||||
state.migrated = true;
|
|
||||||
this.ctx().flags.sync_required = true;
|
|
||||||
|
|
||||||
this.logger.log("setting configs");
|
|
||||||
this.createModules(configs);
|
|
||||||
await this.buildModules();
|
|
||||||
|
|
||||||
this.logger.log("migrated to", version);
|
|
||||||
$console.log("Migrated config from", state.version.before, "to", state.version.after);
|
|
||||||
|
|
||||||
await this.save();
|
|
||||||
} else {
|
|
||||||
this.logger.log("no migrations needed");
|
|
||||||
}
|
|
||||||
|
|
||||||
state.success = true;
|
|
||||||
this.logger.clear();
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
private setConfigs(configs: ModuleConfigs): void {
|
private setConfigs(configs: ModuleConfigs): void {
|
||||||
this.logger.log("setting configs");
|
this.logger.log("setting configs");
|
||||||
objectEach(configs, (config, key) => {
|
objectEach(configs, (config, key) => {
|
||||||
@@ -469,66 +400,66 @@ export class ModuleManager {
|
|||||||
|
|
||||||
async build(opts?: { fetch?: boolean }) {
|
async build(opts?: { fetch?: boolean }) {
|
||||||
this.logger.context("build").log("version", this.version());
|
this.logger.context("build").log("version", this.version());
|
||||||
this.logger.log("booted with", this._booted_with);
|
|
||||||
|
|
||||||
// if no config provided, try fetch from db
|
// if no config provided, try fetch from db
|
||||||
if (this.version() === 0 || opts?.fetch === true) {
|
if (this.version() === 0 || opts?.fetch === true) {
|
||||||
if (this.version() === 0) {
|
if (opts?.fetch) {
|
||||||
this.logger.context("no version").log("version is 0");
|
this.logger.log("force fetch");
|
||||||
} else {
|
|
||||||
this.logger.context("force fetch").log("force fetch");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const result = await this.fetch();
|
||||||
const result = await this.fetch();
|
|
||||||
|
|
||||||
|
// if no version, and nothing found, go with initial
|
||||||
|
if (!result) {
|
||||||
|
this.logger.log("nothing in database, go initial");
|
||||||
|
await this.setupInitial();
|
||||||
|
} else {
|
||||||
|
this.logger.log("db has", result.version);
|
||||||
// set version and config from fetched
|
// set version and config from fetched
|
||||||
this._version = result.version;
|
this._version = result.version;
|
||||||
|
|
||||||
if (this.version() !== CURRENT_VERSION) {
|
|
||||||
await this.syncConfigTable();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.options?.trustFetched === true) {
|
if (this.options?.trustFetched === true) {
|
||||||
this.logger.log("trusting fetched config (mark)");
|
this.logger.log("trusting fetched config (mark)");
|
||||||
mark(result.json);
|
mark(result.json);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setConfigs(result.json);
|
// if version doesn't match, migrate before building
|
||||||
} catch (e: any) {
|
if (this.version() !== CURRENT_VERSION) {
|
||||||
this.logger.clear(); // fetch couldn't clear
|
this.logger.log("now migrating");
|
||||||
|
|
||||||
this.logger.context("error handler").log("fetch failed", e.message);
|
await this.syncConfigTable();
|
||||||
|
|
||||||
// we can safely build modules, since config version is up to date
|
const version_before = this.version();
|
||||||
// it's up to date because we use default configs (no fetch result)
|
const [_version, _configs] = await migrate(version_before, result.json, {
|
||||||
this._version = CURRENT_VERSION;
|
db: this.db,
|
||||||
await this.syncConfigTable();
|
});
|
||||||
const state = await this.buildModules();
|
|
||||||
if (!state.saved) {
|
this._version = _version;
|
||||||
await this.save();
|
this.ctx().flags.sync_required = true;
|
||||||
|
|
||||||
|
this.logger.log("migrated to", _version);
|
||||||
|
$console.log("Migrated config from", version_before, "to", this.version());
|
||||||
|
|
||||||
|
this.createModules(_configs);
|
||||||
|
await this.buildModules();
|
||||||
|
} else {
|
||||||
|
this.logger.log("version is current", this.version());
|
||||||
|
this.createModules(result.json);
|
||||||
|
await this.buildModules();
|
||||||
}
|
}
|
||||||
|
|
||||||
// run initial setup
|
|
||||||
await this.setupInitial();
|
|
||||||
|
|
||||||
this.logger.clear();
|
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
this.logger.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// migrate to latest if needed
|
|
||||||
this.logger.log("check migrate");
|
|
||||||
const migration = await this.migrate();
|
|
||||||
if (migration.success && migration.migrated) {
|
|
||||||
this.logger.log("skipping build after migration");
|
|
||||||
} else {
|
} else {
|
||||||
this.logger.log("trigger build modules");
|
if (this.version() !== CURRENT_VERSION) {
|
||||||
|
throw new Error(
|
||||||
|
`Given version (${this.version()}) and current version (${CURRENT_VERSION}) do not match.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.logger.log("current version is up to date", this.version());
|
||||||
await this.buildModules();
|
await this.buildModules();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log("done");
|
this.logger.log("done");
|
||||||
|
this.logger.clear();
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -589,6 +520,14 @@ export class ModuleManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async setupInitial() {
|
protected async setupInitial() {
|
||||||
|
this.logger.context("initial").log("start");
|
||||||
|
this._version = CURRENT_VERSION;
|
||||||
|
await this.syncConfigTable();
|
||||||
|
const state = await this.buildModules();
|
||||||
|
if (!state.saved) {
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
|
|
||||||
const ctx = {
|
const ctx = {
|
||||||
...this.ctx(),
|
...this.ctx(),
|
||||||
// disable events for initial setup
|
// disable events for initial setup
|
||||||
@@ -601,6 +540,7 @@ export class ModuleManager {
|
|||||||
|
|
||||||
// run first boot event
|
// run first boot event
|
||||||
await this.options?.onFirstBoot?.();
|
await this.options?.onFirstBoot?.();
|
||||||
|
this.logger.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateConfigSafe<Module extends keyof Modules>(
|
mutateConfigSafe<Module extends keyof Modules>(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { _jsonp, transformObject } from "core/utils";
|
import { _jsonp, transformObject } from "core/utils";
|
||||||
import { type Kysely, sql } from "kysely";
|
import { type Kysely, sql } from "kysely";
|
||||||
import { set } from "lodash-es";
|
import { set } from "lodash-es";
|
||||||
|
import type { InitialModuleConfigs } from "modules/ModuleManager";
|
||||||
|
|
||||||
export type MigrationContext = {
|
export type MigrationContext = {
|
||||||
db: Kysely<any>;
|
db: Kysely<any>;
|
||||||
@@ -91,6 +92,17 @@ export const migrations: Migration[] = [
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// remove admin config
|
||||||
|
version: 9,
|
||||||
|
up: async (config) => {
|
||||||
|
const { admin, ...server } = config.server;
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
server,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const CURRENT_VERSION = migrations[migrations.length - 1]?.version ?? 0;
|
export const CURRENT_VERSION = migrations[migrations.length - 1]?.version ?? 0;
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { Fragment } from "hono/jsx";
|
|||||||
import { css, Style } from "hono/css";
|
import { css, Style } from "hono/css";
|
||||||
import { Controller } from "modules/Controller";
|
import { Controller } from "modules/Controller";
|
||||||
import * as SystemPermissions from "modules/permissions";
|
import * as SystemPermissions from "modules/permissions";
|
||||||
import type { AppTheme } from "modules/server/AppServer";
|
|
||||||
|
|
||||||
const htmlBkndContextReplace = "<!-- BKND_CONTEXT -->";
|
const htmlBkndContextReplace = "<!-- BKND_CONTEXT -->";
|
||||||
|
|
||||||
@@ -74,7 +73,6 @@ export class AdminController extends Controller {
|
|||||||
const obj = {
|
const obj = {
|
||||||
user: c.get("auth")?.user,
|
user: c.get("auth")?.user,
|
||||||
logout_route: this.withBasePath(authRoutes.logout),
|
logout_route: this.withBasePath(authRoutes.logout),
|
||||||
color_scheme: configs.server.admin.color_scheme,
|
|
||||||
};
|
};
|
||||||
const html = await this.getHtml(obj);
|
const html = await this.getHtml(obj);
|
||||||
if (!html) {
|
if (!html) {
|
||||||
|
|||||||
@@ -4,24 +4,9 @@ import { cors } from "hono/cors";
|
|||||||
import { Module } from "modules/Module";
|
import { Module } from "modules/Module";
|
||||||
|
|
||||||
const serverMethods = ["GET", "POST", "PATCH", "PUT", "DELETE"];
|
const serverMethods = ["GET", "POST", "PATCH", "PUT", "DELETE"];
|
||||||
const appThemes = ["dark", "light", "system"] as const;
|
|
||||||
export type AppTheme = (typeof appThemes)[number];
|
|
||||||
|
|
||||||
export const serverConfigSchema = Type.Object(
|
export const serverConfigSchema = Type.Object(
|
||||||
{
|
{
|
||||||
admin: Type.Object(
|
|
||||||
{
|
|
||||||
basepath: Type.Optional(Type.String({ default: "", pattern: "^(/.+)?$" })),
|
|
||||||
color_scheme: Type.Optional(StringEnum(["dark", "light", "system"])),
|
|
||||||
logo_return_path: Type.Optional(
|
|
||||||
Type.String({
|
|
||||||
default: "/",
|
|
||||||
description: "Path to return to after *clicking* the logo",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{ default: {}, additionalProperties: false },
|
|
||||||
),
|
|
||||||
cors: Type.Object(
|
cors: Type.Object(
|
||||||
{
|
{
|
||||||
origin: Type.String({ default: "*" }),
|
origin: Type.String({ default: "*" }),
|
||||||
@@ -43,12 +28,6 @@ export const serverConfigSchema = Type.Object(
|
|||||||
|
|
||||||
export type AppServerConfig = Static<typeof serverConfigSchema>;
|
export type AppServerConfig = Static<typeof serverConfigSchema>;
|
||||||
|
|
||||||
/*declare global {
|
|
||||||
interface Request {
|
|
||||||
cf: IncomingRequestCfProperties;
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
|
|
||||||
export class AppServer extends Module<typeof serverConfigSchema> {
|
export class AppServer extends Module<typeof serverConfigSchema> {
|
||||||
//private admin_html?: string;
|
//private admin_html?: string;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { MantineProvider } from "@mantine/core";
|
import { MantineProvider } from "@mantine/core";
|
||||||
import { Notifications } from "@mantine/notifications";
|
import { Notifications } from "@mantine/notifications";
|
||||||
import type { ModuleConfigs } from "modules";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { BkndProvider } from "ui/client/bknd";
|
import { BkndProvider, type BkndAdminOptions } from "ui/client/bknd";
|
||||||
import { useTheme } from "ui/client/use-theme";
|
import { useTheme } from "ui/client/use-theme";
|
||||||
import { Logo } from "ui/components/display/Logo";
|
import { Logo } from "ui/components/display/Logo";
|
||||||
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||||
@@ -14,7 +13,7 @@ import { Routes } from "./routes";
|
|||||||
export type BkndAdminProps = {
|
export type BkndAdminProps = {
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
withProvider?: boolean | ClientProviderProps;
|
withProvider?: boolean | ClientProviderProps;
|
||||||
config?: ModuleConfigs["server"]["admin"];
|
config?: BkndAdminOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Admin({
|
export default function Admin({
|
||||||
@@ -23,7 +22,7 @@ export default function Admin({
|
|||||||
config,
|
config,
|
||||||
}: BkndAdminProps) {
|
}: BkndAdminProps) {
|
||||||
const Component = (
|
const Component = (
|
||||||
<BkndProvider adminOverride={config} fallback={<Skeleton theme={config?.color_scheme} />}>
|
<BkndProvider options={config} fallback={<Skeleton />}>
|
||||||
<AdminInternal />
|
<AdminInternal />
|
||||||
</BkndProvider>
|
</BkndProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ import { createContext, startTransition, useContext, useEffect, useRef, useState
|
|||||||
import { useApi } from "ui/client";
|
import { useApi } from "ui/client";
|
||||||
import { type TSchemaActions, getSchemaActions } from "./schema/actions";
|
import { type TSchemaActions, getSchemaActions } from "./schema/actions";
|
||||||
import { AppReduced } from "./utils/AppReduced";
|
import { AppReduced } from "./utils/AppReduced";
|
||||||
|
import type { AppTheme } from "ui/client/use-theme";
|
||||||
|
|
||||||
|
export type BkndAdminOptions = {
|
||||||
|
logo_return_path?: string;
|
||||||
|
basepath?: string;
|
||||||
|
theme?: AppTheme;
|
||||||
|
};
|
||||||
type BkndContext = {
|
type BkndContext = {
|
||||||
version: number;
|
version: number;
|
||||||
schema: ModuleSchemas;
|
schema: ModuleSchemas;
|
||||||
@@ -14,7 +20,7 @@ type BkndContext = {
|
|||||||
requireSecrets: () => Promise<void>;
|
requireSecrets: () => Promise<void>;
|
||||||
actions: ReturnType<typeof getSchemaActions>;
|
actions: ReturnType<typeof getSchemaActions>;
|
||||||
app: AppReduced;
|
app: AppReduced;
|
||||||
adminOverride?: ModuleConfigs["server"]["admin"];
|
options: BkndAdminOptions;
|
||||||
fallback: boolean;
|
fallback: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -29,13 +35,15 @@ enum Fetching {
|
|||||||
|
|
||||||
export function BkndProvider({
|
export function BkndProvider({
|
||||||
includeSecrets = false,
|
includeSecrets = false,
|
||||||
adminOverride,
|
options,
|
||||||
children,
|
children,
|
||||||
fallback = null,
|
fallback = null,
|
||||||
}: { includeSecrets?: boolean; children: any; fallback?: React.ReactNode } & Pick<
|
}: {
|
||||||
BkndContext,
|
includeSecrets?: boolean;
|
||||||
"adminOverride"
|
children: any;
|
||||||
>) {
|
fallback?: React.ReactNode;
|
||||||
|
options?: BkndAdminOptions;
|
||||||
|
}) {
|
||||||
const [withSecrets, setWithSecrets] = useState<boolean>(includeSecrets);
|
const [withSecrets, setWithSecrets] = useState<boolean>(includeSecrets);
|
||||||
const [schema, setSchema] =
|
const [schema, setSchema] =
|
||||||
useState<Pick<BkndContext, "version" | "schema" | "config" | "permissions" | "fallback">>();
|
useState<Pick<BkndContext, "version" | "schema" | "config" | "permissions" | "fallback">>();
|
||||||
@@ -93,13 +101,6 @@ export function BkndProvider({
|
|||||||
fallback: true,
|
fallback: true,
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
if (adminOverride) {
|
|
||||||
newSchema.config.server.admin = {
|
|
||||||
...newSchema.config.server.admin,
|
|
||||||
...adminOverride,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
document.startViewTransition(() => {
|
document.startViewTransition(() => {
|
||||||
setSchema(newSchema);
|
setSchema(newSchema);
|
||||||
@@ -122,13 +123,13 @@ export function BkndProvider({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!fetched || !schema) return fallback;
|
if (!fetched || !schema) return fallback;
|
||||||
const app = new AppReduced(schema?.config as any);
|
const app = new AppReduced(schema?.config as any, options);
|
||||||
const actions = getSchemaActions({ api, setSchema, reloadSchema });
|
const actions = getSchemaActions({ api, setSchema, reloadSchema });
|
||||||
const hasSecrets = withSecrets && !error;
|
const hasSecrets = withSecrets && !error;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BkndContext.Provider
|
<BkndContext.Provider
|
||||||
value={{ ...schema, actions, requireSecrets, app, adminOverride, hasSecrets }}
|
value={{ ...schema, actions, requireSecrets, app, options: app.options, hasSecrets }}
|
||||||
key={local_version}
|
key={local_version}
|
||||||
>
|
>
|
||||||
{/*{error && (
|
{/*{error && (
|
||||||
@@ -153,3 +154,12 @@ export function useBknd({ withSecrets }: { withSecrets?: boolean } = {}): BkndCo
|
|||||||
|
|
||||||
return ctx;
|
return ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useBkndOptions(): BkndAdminOptions {
|
||||||
|
const ctx = useContext(BkndContext);
|
||||||
|
return (
|
||||||
|
ctx.options ?? {
|
||||||
|
basepath: "/",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Api, type ApiOptions, type TApiUser } from "Api";
|
import { Api, type ApiOptions, type TApiUser } from "Api";
|
||||||
import { isDebug } from "core";
|
import { isDebug } from "core";
|
||||||
import type { AppTheme } from "modules/server/AppServer";
|
|
||||||
import { createContext, useContext } from "react";
|
import { createContext, useContext } from "react";
|
||||||
|
|
||||||
const ClientContext = createContext<{ baseUrl: string; api: Api }>({
|
const ClientContext = createContext<{ baseUrl: string; api: Api }>({
|
||||||
@@ -62,7 +61,6 @@ export const useBaseUrl = () => {
|
|||||||
type BkndWindowContext = {
|
type BkndWindowContext = {
|
||||||
user?: TApiUser;
|
user?: TApiUser;
|
||||||
logout_route: string;
|
logout_route: string;
|
||||||
color_scheme?: AppTheme;
|
|
||||||
};
|
};
|
||||||
export function useBkndWindowContext(): BkndWindowContext {
|
export function useBkndWindowContext(): BkndWindowContext {
|
||||||
if (typeof window !== "undefined" && window.__BKND__) {
|
if (typeof window !== "undefined" && window.__BKND__) {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { BkndProvider, useBknd } from "./BkndProvider";
|
export { BkndProvider, type BkndAdminOptions, useBknd } from "./BkndProvider";
|
||||||
|
|||||||
@@ -29,17 +29,3 @@ export function useBkndSystem() {
|
|||||||
actions,
|
actions,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useBkndSystemTheme() {
|
|
||||||
const $sys = useBkndSystem();
|
|
||||||
|
|
||||||
return {
|
|
||||||
theme: $sys.theme,
|
|
||||||
set: $sys.actions.theme.set,
|
|
||||||
toggle: async () => {
|
|
||||||
document.startViewTransition(async () => {
|
|
||||||
await $sys.actions.theme.toggle();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,29 +1,49 @@
|
|||||||
import type { AppTheme } from "modules/server/AppServer";
|
|
||||||
import { useBkndWindowContext } from "ui/client/ClientProvider";
|
|
||||||
import { useBknd } from "ui/client/bknd";
|
import { useBknd } from "ui/client/bknd";
|
||||||
|
import { create } from "zustand";
|
||||||
|
import { combine, persist } from "zustand/middleware";
|
||||||
|
|
||||||
|
const themes = ["dark", "light", "system"] as const;
|
||||||
|
export type AppTheme = (typeof themes)[number];
|
||||||
|
|
||||||
|
const themeStore = create(
|
||||||
|
persist(
|
||||||
|
combine({ theme: null as AppTheme | null }, (set) => ({
|
||||||
|
setTheme: (theme: AppTheme | any) => {
|
||||||
|
if (themes.includes(theme)) {
|
||||||
|
document.startViewTransition(() => {
|
||||||
|
set({ theme });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
name: "bknd-admin-theme",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
export function useTheme(fallback: AppTheme = "system") {
|
export function useTheme(fallback: AppTheme = "system") {
|
||||||
const b = useBknd();
|
const b = useBknd();
|
||||||
const winCtx = useBkndWindowContext();
|
const theme_state = themeStore((state) => state.theme);
|
||||||
|
const theme_set = themeStore((state) => state.setTheme);
|
||||||
|
|
||||||
// 1. override
|
// 1. override
|
||||||
// 2. config
|
// 2. local storage
|
||||||
// 3. winCtx
|
// 3. fallback
|
||||||
// 4. fallback
|
// 4. default
|
||||||
// 5. default
|
const override = b?.options?.theme;
|
||||||
const override = b?.adminOverride?.color_scheme;
|
|
||||||
const config = b?.config.server.admin.color_scheme;
|
|
||||||
const win = winCtx.color_scheme;
|
|
||||||
const prefersDark =
|
const prefersDark =
|
||||||
typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches;
|
typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||||
|
|
||||||
const theme = override ?? config ?? win ?? fallback;
|
const theme = override ?? theme_state ?? fallback;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
theme: (theme === "system" ? (prefersDark ? "dark" : "light") : theme) as AppTheme,
|
theme: (theme === "system" ? (prefersDark ? "dark" : "light") : theme) as AppTheme,
|
||||||
|
value: theme,
|
||||||
|
themes,
|
||||||
|
setTheme: theme_set,
|
||||||
|
state: theme_state,
|
||||||
prefersDark,
|
prefersDark,
|
||||||
override,
|
override,
|
||||||
config,
|
|
||||||
win,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { App } from "App";
|
|||||||
import { type Entity, type EntityRelation, constructEntity, constructRelation } from "data";
|
import { type Entity, type EntityRelation, constructEntity, constructRelation } from "data";
|
||||||
import { RelationAccessor } from "data/relations/RelationAccessor";
|
import { RelationAccessor } from "data/relations/RelationAccessor";
|
||||||
import { Flow, TaskMap } from "flows";
|
import { Flow, TaskMap } from "flows";
|
||||||
|
import type { BkndAdminOptions } from "ui/client/BkndProvider";
|
||||||
|
|
||||||
export type AppType = ReturnType<App["toJSON"]>;
|
export type AppType = ReturnType<App["toJSON"]>;
|
||||||
|
|
||||||
@@ -15,7 +16,10 @@ export class AppReduced {
|
|||||||
private _relations: EntityRelation[] = [];
|
private _relations: EntityRelation[] = [];
|
||||||
private _flows: Flow[] = [];
|
private _flows: Flow[] = [];
|
||||||
|
|
||||||
constructor(protected appJson: AppType) {
|
constructor(
|
||||||
|
protected appJson: AppType,
|
||||||
|
protected _options: BkndAdminOptions = {},
|
||||||
|
) {
|
||||||
//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]) => {
|
||||||
@@ -62,18 +66,21 @@ export class AppReduced {
|
|||||||
return this.appJson;
|
return this.appJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAdminConfig() {
|
get options() {
|
||||||
return this.appJson.server.admin;
|
return {
|
||||||
|
basepath: "",
|
||||||
|
logo_return_path: "/",
|
||||||
|
...this._options,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getSettingsPath(path: string[] = []): string {
|
getSettingsPath(path: string[] = []): string {
|
||||||
const { basepath } = this.getAdminConfig();
|
const base = `~/${this.options.basepath}/settings`.replace(/\/+/g, "/");
|
||||||
const base = `~/${basepath}/settings`.replace(/\/+/g, "/");
|
|
||||||
return [base, ...path].join("/");
|
return [base, ...path].join("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
getAbsolutePath(path?: string): string {
|
getAbsolutePath(path?: string): string {
|
||||||
const { basepath } = this.getAdminConfig();
|
const { basepath } = this.options;
|
||||||
return (path ? `~/${basepath}/${path}` : `~/${basepath}`).replace(/\/+/g, "/");
|
return (path ? `~/${basepath}/${path}` : `~/${basepath}`).replace(/\/+/g, "/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
export type AppTheme = "light" | "dark" | string;
|
|
||||||
|
|
||||||
export function useSetTheme(initialTheme: AppTheme = "light") {
|
|
||||||
const [theme, _setTheme] = useState(initialTheme);
|
|
||||||
|
|
||||||
const $html = document.querySelector("#bknd-admin")!;
|
|
||||||
function setTheme(newTheme: AppTheme) {
|
|
||||||
$html?.classList.remove("dark", "light");
|
|
||||||
$html?.classList.add(newTheme);
|
|
||||||
_setTheme(newTheme);
|
|
||||||
|
|
||||||
// @todo: just a quick switcher config update test
|
|
||||||
fetch("/api/system/config/patch/server/admin", {
|
|
||||||
method: "PATCH",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ color_scheme: newTheme }),
|
|
||||||
})
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((data) => {
|
|
||||||
console.log("theme updated", data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { theme, setTheme };
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,17 @@
|
|||||||
import {
|
import {
|
||||||
Background,
|
Background,
|
||||||
BackgroundVariant,
|
BackgroundVariant,
|
||||||
MarkerType,
|
|
||||||
MiniMap,
|
MiniMap,
|
||||||
type MiniMapProps,
|
type MiniMapProps,
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
type ReactFlowProps,
|
type ReactFlowProps,
|
||||||
ReactFlowProvider,
|
|
||||||
addEdge,
|
addEdge,
|
||||||
useEdgesState,
|
useEdgesState,
|
||||||
useNodesState,
|
useNodesState,
|
||||||
useReactFlow,
|
useReactFlow,
|
||||||
} from "@xyflow/react";
|
} from "@xyflow/react";
|
||||||
import { type ReactNode, useCallback, useEffect, useState } from "react";
|
import { type ReactNode, useCallback, useEffect, useState } from "react";
|
||||||
import { useBkndSystemTheme } from "ui/client/schema/system/use-bknd-system";
|
import { useTheme } from "ui/client/use-theme";
|
||||||
|
|
||||||
type CanvasProps = ReactFlowProps & {
|
type CanvasProps = ReactFlowProps & {
|
||||||
externalProvider?: boolean;
|
externalProvider?: boolean;
|
||||||
@@ -38,7 +36,7 @@ export function Canvas({
|
|||||||
const [nodes, setNodes, onNodesChange] = useNodesState(_nodes ?? []);
|
const [nodes, setNodes, onNodesChange] = useNodesState(_nodes ?? []);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState(_edges ?? []);
|
const [edges, setEdges, onEdgesChange] = useEdgesState(_edges ?? []);
|
||||||
const { screenToFlowPosition } = useReactFlow();
|
const { screenToFlowPosition } = useReactFlow();
|
||||||
const { theme } = useBkndSystemTheme();
|
const { theme } = useTheme();
|
||||||
|
|
||||||
const [isCommandPressed, setIsCommandPressed] = useState(false);
|
const [isCommandPressed, setIsCommandPressed] = useState(false);
|
||||||
const [isSpacePressed, setIsSpacePressed] = useState(false);
|
const [isSpacePressed, setIsSpacePressed] = useState(false);
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import type { ComponentPropsWithoutRef } from "react";
|
|||||||
import { Button } from "ui/components/buttons/Button";
|
import { Button } from "ui/components/buttons/Button";
|
||||||
import { Group, Input, Label } from "ui/components/form/Formy/components";
|
import { Group, Input, Label } from "ui/components/form/Formy/components";
|
||||||
import { SocialLink } from "./SocialLink";
|
import { SocialLink } from "./SocialLink";
|
||||||
|
|
||||||
import type { ValueError } from "@sinclair/typebox/value";
|
import type { ValueError } from "@sinclair/typebox/value";
|
||||||
import { type TSchema, Value } from "core/utils";
|
import { type TSchema, Value } from "core/utils";
|
||||||
import type { Validator } from "json-schema-form-react";
|
import type { Validator } from "json-schema-form-react";
|
||||||
|
import { useTheme } from "ui/client/use-theme";
|
||||||
|
|
||||||
class TypeboxValidator implements Validator<ValueError> {
|
class TypeboxValidator implements Validator<ValueError> {
|
||||||
async validate(schema: TSchema, data: any) {
|
async validate(schema: TSchema, data: any) {
|
||||||
@@ -46,6 +46,7 @@ export function AuthForm({
|
|||||||
buttonLabel = action === "login" ? "Sign in" : "Sign up",
|
buttonLabel = action === "login" ? "Sign in" : "Sign up",
|
||||||
...props
|
...props
|
||||||
}: LoginFormProps) {
|
}: LoginFormProps) {
|
||||||
|
const { theme } = useTheme();
|
||||||
const basepath = auth?.basepath ?? "/api/auth";
|
const basepath = auth?.basepath ?? "/api/auth";
|
||||||
const password = {
|
const password = {
|
||||||
action: `${basepath}/password/${action}`,
|
action: `${basepath}/password/${action}`,
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
} from "react-icons/tb";
|
} from "react-icons/tb";
|
||||||
import { useAuth, useBkndWindowContext } from "ui/client";
|
import { useAuth, useBkndWindowContext } from "ui/client";
|
||||||
import { useBknd } from "ui/client/bknd";
|
import { useBknd } from "ui/client/bknd";
|
||||||
import { useBkndSystemTheme } from "ui/client/schema/system/use-bknd-system";
|
|
||||||
import { useTheme } from "ui/client/use-theme";
|
import { useTheme } from "ui/client/use-theme";
|
||||||
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";
|
||||||
@@ -24,6 +23,7 @@ import { useAppShell } from "ui/layouts/AppShell/use-appshell";
|
|||||||
import { useNavigate } from "ui/lib/routes";
|
import { useNavigate } from "ui/lib/routes";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { NavLink } from "./AppShell";
|
import { NavLink } from "./AppShell";
|
||||||
|
import { autoFormatString } from "core/utils";
|
||||||
|
|
||||||
export function HeaderNavigation() {
|
export function HeaderNavigation() {
|
||||||
const [location, navigate] = useLocation();
|
const [location, navigate] = useLocation();
|
||||||
@@ -114,7 +114,7 @@ function SidebarToggler() {
|
|||||||
export function Header({ hasSidebar = true }) {
|
export function Header({ hasSidebar = true }) {
|
||||||
const { app } = useBknd();
|
const { app } = useBknd();
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const { logo_return_path = "/" } = app.getAdminConfig();
|
const { logo_return_path = "/" } = app.options;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
@@ -142,7 +142,7 @@ export function Header({ hasSidebar = true }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function UserMenu() {
|
function UserMenu() {
|
||||||
const { adminOverride, config } = useBknd();
|
const { config, options } = useBknd();
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
const [navigate] = useNavigate();
|
const [navigate] = useNavigate();
|
||||||
const { logout_route } = useBkndWindowContext();
|
const { logout_route } = useBkndWindowContext();
|
||||||
@@ -173,7 +173,7 @@ function UserMenu() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!adminOverride) {
|
if (!options.theme) {
|
||||||
items.push(() => <UserMenuThemeToggler />);
|
items.push(() => <UserMenuThemeToggler />);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,17 +193,15 @@ function UserMenu() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function UserMenuThemeToggler() {
|
function UserMenuThemeToggler() {
|
||||||
const { theme, toggle } = useBkndSystemTheme();
|
const { value, themes, setTheme } = useTheme();
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center mt-1 pt-1 border-t border-primary/5">
|
<div className="flex flex-col items-center mt-1 pt-1 border-t border-primary/5">
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
|
withItemsBorders={false}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
data={[
|
data={themes.map((t) => ({ value: t, label: autoFormatString(t) }))}
|
||||||
{ value: "light", label: "Light" },
|
value={value}
|
||||||
{ value: "dark", label: "Dark" },
|
onChange={setTheme}
|
||||||
]}
|
|
||||||
value={theme}
|
|
||||||
onChange={toggle}
|
|
||||||
size="xs"
|
size="xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -49,8 +49,7 @@ export function withQuery(url: string, query: object) {
|
|||||||
|
|
||||||
export function withAbsolute(url: string) {
|
export function withAbsolute(url: string) {
|
||||||
const { app } = useBknd();
|
const { app } = useBknd();
|
||||||
const basepath = app.getAdminConfig().basepath;
|
return app.getAbsolutePath(url);
|
||||||
return `~/${basepath}/${url}`.replace(/\/+/g, "/");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRouteNavigate() {
|
export function useRouteNavigate() {
|
||||||
@@ -65,7 +64,7 @@ export function useNavigate() {
|
|||||||
const [location, navigate] = useLocation();
|
const [location, navigate] = useLocation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { app } = useBknd();
|
const { app } = useBknd();
|
||||||
const basepath = app.getAdminConfig().basepath;
|
const basepath = app.options.basepath;
|
||||||
return [
|
return [
|
||||||
(
|
(
|
||||||
url: string,
|
url: string,
|
||||||
@@ -121,7 +120,6 @@ export function useGoBack(
|
|||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const { app } = useBknd();
|
const { app } = useBknd();
|
||||||
const basepath = app.getAdminConfig().basepath;
|
|
||||||
const [navigate] = useNavigate();
|
const [navigate] = useNavigate();
|
||||||
const referrer = document.referrer;
|
const referrer = document.referrer;
|
||||||
const history_length = window.history.length;
|
const history_length = window.history.length;
|
||||||
@@ -142,9 +140,7 @@ export function useGoBack(
|
|||||||
} else {
|
} else {
|
||||||
//console.log("used fallback");
|
//console.log("used fallback");
|
||||||
if (typeof fallback === "string") {
|
if (typeof fallback === "string") {
|
||||||
const _fallback = options?.absolute
|
const _fallback = options?.absolute ? app.getAbsolutePath(fallback) : fallback;
|
||||||
? `~/${basepath}${fallback}`.replace(/\/+/g, "/")
|
|
||||||
: fallback;
|
|
||||||
//console.log("fallback", _fallback);
|
//console.log("fallback", _fallback);
|
||||||
|
|
||||||
if (options?.native) {
|
if (options?.native) {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dark,
|
.dark,
|
||||||
|
.dark .bknd-admin /* currently used for elements, drop after making headless */,
|
||||||
#bknd-admin.dark,
|
#bknd-admin.dark,
|
||||||
.bknd-admin.dark {
|
.bknd-admin.dark {
|
||||||
--color-primary: rgb(250 250 250); /* zinc-50 */
|
--color-primary: rgb(250 250 250); /* zinc-50 */
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { MarkerType, type Node, Position, ReactFlowProvider } from "@xyflow/react";
|
import { MarkerType, type Node, Position, ReactFlowProvider } from "@xyflow/react";
|
||||||
import type { AppDataConfig, TAppDataEntity } from "data/data-schema";
|
import type { AppDataConfig, TAppDataEntity } from "data/data-schema";
|
||||||
import { useBknd } from "ui/client/BkndProvider";
|
import { useBknd } from "ui/client/BkndProvider";
|
||||||
import { useBkndSystemTheme } from "ui/client/schema/system/use-bknd-system";
|
|
||||||
import { Canvas } from "ui/components/canvas/Canvas";
|
import { Canvas } from "ui/components/canvas/Canvas";
|
||||||
import { layoutWithDagre } from "ui/components/canvas/layouts";
|
import { layoutWithDagre } from "ui/components/canvas/layouts";
|
||||||
import { Panels } from "ui/components/canvas/panels";
|
import { Panels } from "ui/components/canvas/panels";
|
||||||
import { EntityTableNode } from "./EntityTableNode";
|
import { EntityTableNode } from "./EntityTableNode";
|
||||||
|
import { useTheme } from "ui/client/use-theme";
|
||||||
|
|
||||||
function entitiesToNodes(entities: AppDataConfig["entities"]): Node<TAppDataEntity>[] {
|
function entitiesToNodes(entities: AppDataConfig["entities"]): Node<TAppDataEntity>[] {
|
||||||
return Object.entries(entities ?? {}).map(([name, entity]) => {
|
return Object.entries(entities ?? {}).map(([name, entity]) => {
|
||||||
@@ -69,7 +69,7 @@ export function DataSchemaCanvas() {
|
|||||||
const {
|
const {
|
||||||
config: { data },
|
config: { data },
|
||||||
} = useBknd();
|
} = useBknd();
|
||||||
const { theme } = useBkndSystemTheme();
|
const { theme } = useTheme();
|
||||||
const nodes = entitiesToNodes(data.entities);
|
const nodes = entitiesToNodes(data.entities);
|
||||||
const edges = relationsToEdges(data.relations).map((e) => ({
|
const edges = relationsToEdges(data.relations).map((e) => ({
|
||||||
...e,
|
...e,
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ import { Dropdown } from "../../components/overlay/Dropdown";
|
|||||||
import { useFlow } from "../../container/use-flows";
|
import { useFlow } from "../../container/use-flows";
|
||||||
import * as AppShell from "../../layouts/AppShell/AppShell";
|
import * as AppShell from "../../layouts/AppShell/AppShell";
|
||||||
import { SectionHeader } from "../../layouts/AppShell/AppShell";
|
import { SectionHeader } from "../../layouts/AppShell/AppShell";
|
||||||
|
import { useTheme } from "ui/client/use-theme";
|
||||||
|
|
||||||
export function FlowEdit({ params }) {
|
export function FlowEdit({ params }) {
|
||||||
const { app } = useBknd();
|
const { app } = useBknd();
|
||||||
const { color_scheme: theme } = app.getAdminConfig();
|
const { theme } = useTheme();
|
||||||
const { basepath } = app.getAdminConfig();
|
const prefix = app.getAbsolutePath("settings");
|
||||||
const prefix = `~/${basepath}/settings`.replace(/\/+/g, "/");
|
|
||||||
const [location, navigate] = useLocation();
|
const [location, navigate] = useLocation();
|
||||||
const [execution, setExecution] = useState<Execution>();
|
const [execution, setExecution] = useState<Execution>();
|
||||||
const [selectedNodes, setSelectedNodes] = useState<Node[]>([]);
|
const [selectedNodes, setSelectedNodes] = useState<Node[]>([]);
|
||||||
|
|||||||
@@ -17,12 +17,11 @@ const TestRoutes = lazy(() => import("./test"));
|
|||||||
export function Routes() {
|
export function Routes() {
|
||||||
const { app } = useBknd();
|
const { app } = useBknd();
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const { basepath } = app.getAdminConfig();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="bknd-admin" className={theme + " antialiased"}>
|
<div id="bknd-admin" className={theme + " antialiased"}>
|
||||||
<FlashMessage />
|
<FlashMessage />
|
||||||
<Router base={basepath}>
|
<Router base={app.options.basepath}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/auth/login" component={AuthLogin} />
|
<Route path="/auth/login" component={AuthLogin} />
|
||||||
<Route path="/" nest>
|
<Route path="/" nest>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { ServerSettings } from "./routes/server.settings";
|
|||||||
import { IconButton } from "ui/components/buttons/IconButton";
|
import { IconButton } from "ui/components/buttons/IconButton";
|
||||||
|
|
||||||
function SettingsSidebar() {
|
function SettingsSidebar() {
|
||||||
const { version, schema, actions } = useBknd();
|
const { version, schema, actions, app } = useBknd();
|
||||||
useBrowserTitle(["Settings"]);
|
useBrowserTitle(["Settings"]);
|
||||||
|
|
||||||
async function handleRefresh() {
|
async function handleRefresh() {
|
||||||
@@ -151,11 +151,10 @@ const FallbackRoutes = ({
|
|||||||
...settingProps
|
...settingProps
|
||||||
}: SettingProps<any> & { module: string }) => {
|
}: SettingProps<any> & { module: string }) => {
|
||||||
const { app } = useBknd();
|
const { app } = useBknd();
|
||||||
const basepath = app.getAdminConfig();
|
const prefix = app.getAbsolutePath("settings");
|
||||||
const prefix = `~/${basepath}/settings`.replace(/\/+/g, "/");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Route path={`/${module}`} nest>
|
<Route path={module} nest>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route
|
<Route
|
||||||
path="/"
|
path="/"
|
||||||
|
|||||||
@@ -44,8 +44,7 @@ const uiSchema = {
|
|||||||
export const AuthSettings = ({ schema: _unsafe_copy, config }) => {
|
export const AuthSettings = ({ schema: _unsafe_copy, config }) => {
|
||||||
const _s = useBknd();
|
const _s = useBknd();
|
||||||
const _schema = cloneDeep(_unsafe_copy);
|
const _schema = cloneDeep(_unsafe_copy);
|
||||||
const { basepath } = _s.app.getAdminConfig();
|
const prefix = _s.app.getAbsolutePath("settings");
|
||||||
const prefix = `~/${basepath}/settings`.replace(/\/+/g, "/");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user_entity = config.entity_name ?? "users";
|
const user_entity = config.entity_name ?? "users";
|
||||||
|
|||||||
@@ -68,8 +68,7 @@ export const DataSettings = ({
|
|||||||
config,
|
config,
|
||||||
}: { schema: ModuleSchemas["data"]; config: ModuleConfigs["data"] }) => {
|
}: { schema: ModuleSchemas["data"]; config: ModuleConfigs["data"] }) => {
|
||||||
const { app } = useBknd();
|
const { app } = useBknd();
|
||||||
const basepath = app.getAdminConfig().basepath;
|
const prefix = app.getAbsolutePath("settings");
|
||||||
const prefix = `~/${basepath}/settings`.replace(/\/+/g, "/");
|
|
||||||
const entities = Object.keys(config.entities ?? {});
|
const entities = Object.keys(config.entities ?? {});
|
||||||
|
|
||||||
function fillEntities(schema: any, key: string = "entity") {
|
function fillEntities(schema: any, key: string = "entity") {
|
||||||
|
|||||||
@@ -31,8 +31,7 @@ const uiSchema = {
|
|||||||
|
|
||||||
export const FlowsSettings = ({ schema, config }) => {
|
export const FlowsSettings = ({ schema, config }) => {
|
||||||
const { app } = useBknd();
|
const { app } = useBknd();
|
||||||
const { basepath } = app.getAdminConfig();
|
const prefix = app.getAbsolutePath("settings");
|
||||||
const prefix = `~/${basepath}/settings`.replace(/\/+/g, "/");
|
|
||||||
|
|
||||||
function fillTasks(schema: any, flow: any, key: string) {
|
function fillTasks(schema: any, flow: any, key: string) {
|
||||||
const tasks = Object.keys(flow.tasks ?? {});
|
const tasks = Object.keys(flow.tasks ?? {});
|
||||||
|
|||||||
@@ -17,15 +17,11 @@ const uiSchema = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ServerSettings = ({ schema: _unsafe_copy, config }) => {
|
export const ServerSettings = ({ schema: _unsafe_copy, config }) => {
|
||||||
const { app, adminOverride } = useBknd();
|
const { app } = useBknd();
|
||||||
const { basepath } = app.getAdminConfig();
|
|
||||||
const _schema = cloneDeep(_unsafe_copy);
|
const _schema = cloneDeep(_unsafe_copy);
|
||||||
const prefix = `~/${basepath}/settings`.replace(/\/+/g, "/");
|
const prefix = app.getAbsolutePath("settings");
|
||||||
|
|
||||||
const schema = _schema;
|
const schema = _schema;
|
||||||
if (adminOverride) {
|
|
||||||
schema.properties.admin.readOnly = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Route path="/server" nest>
|
<Route path="/server" nest>
|
||||||
@@ -33,14 +29,6 @@ export const ServerSettings = ({ schema: _unsafe_copy, config }) => {
|
|||||||
path="/"
|
path="/"
|
||||||
component={() => (
|
component={() => (
|
||||||
<Setting
|
<Setting
|
||||||
options={{
|
|
||||||
showAlert: () => {
|
|
||||||
if (adminOverride) {
|
|
||||||
return "The admin settings are read-only as they are overriden. Remaining server configuration can be edited.";
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
schema={schema}
|
schema={schema}
|
||||||
uiSchema={uiSchema}
|
uiSchema={uiSchema}
|
||||||
config={config}
|
config={config}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { serveStatic } from "@hono/node-server/serve-static";
|
|||||||
import { showRoutes } from "hono/dev";
|
import { showRoutes } from "hono/dev";
|
||||||
import { App, registries } from "./src";
|
import { App, registries } from "./src";
|
||||||
import { StorageLocalAdapter } from "./src/media/storage/adapters/StorageLocalAdapter";
|
import { StorageLocalAdapter } from "./src/media/storage/adapters/StorageLocalAdapter";
|
||||||
|
import { EntityManager, LibsqlConnection } from "data";
|
||||||
|
import { __bknd } from "modules/ModuleManager";
|
||||||
|
|
||||||
registries.media.register("local", StorageLocalAdapter);
|
registries.media.register("local", StorageLocalAdapter);
|
||||||
|
|
||||||
@@ -21,10 +23,30 @@ const credentials = example
|
|||||||
url: ":memory:",
|
url: ":memory:",
|
||||||
};
|
};
|
||||||
|
|
||||||
let initialConfig: any = undefined;
|
|
||||||
if (example) {
|
if (example) {
|
||||||
const { version, ...config } = JSON.parse(await readFile(`.configs/${example}.json`, "utf-8"));
|
const { version, ...config } = JSON.parse(await readFile(`.configs/${example}.json`, "utf-8"));
|
||||||
initialConfig = config;
|
|
||||||
|
// create db with config
|
||||||
|
const conn = new LibsqlConnection(credentials);
|
||||||
|
const em = new EntityManager([__bknd], conn);
|
||||||
|
try {
|
||||||
|
await em.schema().sync({ force: true });
|
||||||
|
} catch (e) {}
|
||||||
|
const { data: existing } = await em.repo(__bknd).findOne({ type: "config" });
|
||||||
|
|
||||||
|
if (!existing || existing.version !== version) {
|
||||||
|
if (existing) await em.mutator(__bknd).deleteOne(existing.id);
|
||||||
|
await em.mutator(__bknd).insertOne({
|
||||||
|
version,
|
||||||
|
json: config,
|
||||||
|
created_at: new Date(),
|
||||||
|
type: "config",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await em.mutator(__bknd).updateOne(existing.id, {
|
||||||
|
json: config,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let app: App;
|
let app: App;
|
||||||
@@ -35,7 +57,6 @@ export default {
|
|||||||
if (!app || recreate) {
|
if (!app || recreate) {
|
||||||
app = App.create({
|
app = App.create({
|
||||||
connection: credentials,
|
connection: credentials,
|
||||||
initialConfig,
|
|
||||||
});
|
});
|
||||||
app.emgr.onEvent(
|
app.emgr.onEvent(
|
||||||
App.Events.AppBuiltEvent,
|
App.Events.AppBuiltEvent,
|
||||||
|
|||||||
Reference in New Issue
Block a user