mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
feat: improved abilities of plugins, moved schema fns to ctx
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
import { afterAll, afterEach, describe, expect, test } from "bun:test";
|
import { afterAll, afterEach, describe, expect, test } from "bun:test";
|
||||||
import { App } from "../src";
|
import { App } from "../src";
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
|
import { Hono } from "hono";
|
||||||
|
import * as proto from "../src/data/prototype";
|
||||||
|
import { pick } from "lodash-es";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
afterEach(afterAllCleanup);
|
afterEach(afterAllCleanup);
|
||||||
@@ -10,18 +13,91 @@ describe("App tests", async () => {
|
|||||||
const app = new App(dummyConnection);
|
const app = new App(dummyConnection);
|
||||||
await app.build();
|
await app.build();
|
||||||
|
|
||||||
//expect(await app.data?.em.ping()).toBeTrue();
|
expect(await app.em.ping()).toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
/*test.only("what", async () => {
|
test("plugins", async () => {
|
||||||
const app = new App(dummyConnection, {
|
const called: string[] = [];
|
||||||
auth: {
|
const app = App.create({
|
||||||
enabled: true,
|
initialConfig: {
|
||||||
|
auth: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
plugins: [
|
||||||
|
(app) => {
|
||||||
|
expect(app).toBeDefined();
|
||||||
|
expect(app).toBeInstanceOf(App);
|
||||||
|
return {
|
||||||
|
name: "test",
|
||||||
|
schema: () => {
|
||||||
|
called.push("schema");
|
||||||
|
return proto.em(
|
||||||
|
{
|
||||||
|
posts: proto.entity("posts", {
|
||||||
|
title: proto.text(),
|
||||||
|
}),
|
||||||
|
comments: proto.entity("comments", {
|
||||||
|
content: proto.text(),
|
||||||
|
}),
|
||||||
|
users: proto.entity("users", {
|
||||||
|
email_verified: proto.boolean(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
(fn, s) => {
|
||||||
|
fn.relation(s.comments).manyToOne(s.posts);
|
||||||
|
fn.index(s.posts).on(["title"]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
beforeBuild: async () => {
|
||||||
|
called.push("beforeBuild");
|
||||||
|
},
|
||||||
|
onBuilt: async () => {
|
||||||
|
called.push("onBuilt");
|
||||||
|
},
|
||||||
|
onServerInit: async (server) => {
|
||||||
|
called.push("onServerInit");
|
||||||
|
expect(server).toBeDefined();
|
||||||
|
expect(server).toBeInstanceOf(Hono);
|
||||||
|
},
|
||||||
|
onFirstBoot: async () => {
|
||||||
|
called.push("onFirstBoot");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await app.module.auth.build();
|
|
||||||
await app.module.data.build();
|
await app.build();
|
||||||
console.log(app.em.entities.map((e) => e.name));
|
|
||||||
console.log(await app.em.schema().getDiff());
|
expect(app.em.entities.map((e) => e.name)).toEqual(["users", "posts", "comments"]);
|
||||||
});*/
|
expect(app.em.indices.map((i) => i.name)).toEqual([
|
||||||
|
"idx_unique_users_email",
|
||||||
|
"idx_users_strategy",
|
||||||
|
"idx_users_strategy_value",
|
||||||
|
"idx_posts_title",
|
||||||
|
]);
|
||||||
|
expect(
|
||||||
|
app.em.relations.all.map((r) => pick(r.toJSON(), ["type", "source", "target"])),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
type: "n:1",
|
||||||
|
source: "comments",
|
||||||
|
target: "posts",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(called).toEqual([
|
||||||
|
"onServerInit",
|
||||||
|
"beforeBuild",
|
||||||
|
"onServerInit",
|
||||||
|
"schema",
|
||||||
|
"onFirstBoot",
|
||||||
|
"onBuilt",
|
||||||
|
]);
|
||||||
|
expect(app.plugins).toHaveLength(1);
|
||||||
|
expect(app.plugins.map((p) => p.name)).toEqual(["test"]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ describe("App", () => {
|
|||||||
"guard",
|
"guard",
|
||||||
"flags",
|
"flags",
|
||||||
"logger",
|
"logger",
|
||||||
|
"helper",
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { type TSchema, Type } from "@sinclair/typebox";
|
|||||||
import { EntityManager, em, entity, index, text } from "../../src/data";
|
import { EntityManager, em, entity, index, text } from "../../src/data";
|
||||||
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
||||||
import { Module } from "../../src/modules/Module";
|
import { Module } from "../../src/modules/Module";
|
||||||
|
import { ModuleHelper } from "modules/ModuleHelper";
|
||||||
|
|
||||||
function createModule<Schema extends TSchema>(schema: Schema) {
|
function createModule<Schema extends TSchema>(schema: Schema) {
|
||||||
class TestModule extends Module<typeof schema> {
|
class TestModule extends Module<typeof schema> {
|
||||||
@@ -46,9 +47,9 @@ describe("Module", async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prt = {
|
prt = {
|
||||||
ensureEntity: this.ensureEntity.bind(this),
|
ensureEntity: this.ctx.helper.ensureEntity.bind(this.ctx.helper),
|
||||||
ensureIndex: this.ensureIndex.bind(this),
|
ensureIndex: this.ctx.helper.ensureIndex.bind(this.ctx.helper),
|
||||||
ensureSchema: this.ensureSchema.bind(this),
|
ensureSchema: this.ctx.helper.ensureSchema.bind(this.ctx.helper),
|
||||||
};
|
};
|
||||||
|
|
||||||
get em() {
|
get em() {
|
||||||
@@ -63,7 +64,11 @@ describe("Module", async () => {
|
|||||||
_em.relations,
|
_em.relations,
|
||||||
_em.indices,
|
_em.indices,
|
||||||
);
|
);
|
||||||
return new M({} as any, { em, flags: Module.ctx_flags } as any);
|
const ctx = {
|
||||||
|
em,
|
||||||
|
flags: Module.ctx_flags,
|
||||||
|
};
|
||||||
|
return new M({} as any, { ...ctx, helper: new ModuleHelper(ctx as any) } as any);
|
||||||
}
|
}
|
||||||
function flat(_em: EntityManager) {
|
function flat(_em: EntityManager) {
|
||||||
return {
|
return {
|
||||||
@@ -143,14 +148,9 @@ describe("Module", async () => {
|
|||||||
|
|
||||||
// this should only add the field "important"
|
// this should only add the field "important"
|
||||||
m.prt.ensureEntity(
|
m.prt.ensureEntity(
|
||||||
entity(
|
entity("u", {
|
||||||
"u",
|
important: text(),
|
||||||
{
|
}),
|
||||||
important: text(),
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
"system",
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(m.ctx.flags.sync_required).toBe(true);
|
expect(m.ctx.flags.sync_required).toBe(true);
|
||||||
@@ -159,8 +159,7 @@ describe("Module", async () => {
|
|||||||
{
|
{
|
||||||
name: "u",
|
name: "u",
|
||||||
fields: ["id", "name", "important"],
|
fields: ["id", "name", "important"],
|
||||||
// ensured type must be present
|
type: "regular",
|
||||||
type: "system",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "p",
|
name: "p",
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ import { Default, stripMark } from "../../src/core/utils";
|
|||||||
import { EntityManager } from "../../src/data";
|
import { EntityManager } from "../../src/data";
|
||||||
import { Module, type ModuleBuildContext } from "../../src/modules/Module";
|
import { Module, type ModuleBuildContext } from "../../src/modules/Module";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
|
import { ModuleHelper } from "modules/ModuleHelper";
|
||||||
|
|
||||||
export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildContext {
|
export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildContext {
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
return {
|
const ctx = {
|
||||||
connection: dummyConnection,
|
connection: dummyConnection,
|
||||||
server: new Hono(),
|
server: new Hono(),
|
||||||
em: new EntityManager([], dummyConnection),
|
em: new EntityManager([], dummyConnection),
|
||||||
@@ -21,6 +22,10 @@ export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildCon
|
|||||||
logger: new DebugLogger(false),
|
logger: new DebugLogger(false),
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
|
return {
|
||||||
|
...ctx,
|
||||||
|
helper: new ModuleHelper(ctx as any),
|
||||||
|
} as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function moduleTestSuite(module: { new (): Module }) {
|
export function moduleTestSuite(module: { new (): Module }) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { CreateUserPayload } from "auth/AppAuth";
|
import type { CreateUserPayload } from "auth/AppAuth";
|
||||||
import { $console } from "core";
|
import { $console } from "core";
|
||||||
import { Event } from "core/events";
|
import { Event } from "core/events";
|
||||||
import { Connection, type LibSqlCredentials, LibsqlConnection } from "data";
|
import { Connection, type LibSqlCredentials, LibsqlConnection, type em as prototypeEm } from "data";
|
||||||
import type { Hono } from "hono";
|
import type { Hono } from "hono";
|
||||||
import {
|
import {
|
||||||
ModuleManager,
|
ModuleManager,
|
||||||
@@ -14,12 +14,21 @@ import {
|
|||||||
import * as SystemPermissions from "modules/permissions";
|
import * as SystemPermissions from "modules/permissions";
|
||||||
import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
|
import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
|
||||||
import { SystemController } from "modules/server/SystemController";
|
import { SystemController } from "modules/server/SystemController";
|
||||||
|
import type { MaybePromise } from "core/types";
|
||||||
|
import type { ServerEnv } from "modules/Controller";
|
||||||
|
|
||||||
// biome-ignore format: must be here
|
// biome-ignore format: must be here
|
||||||
import { Api, type ApiOptions } from "Api";
|
import { Api, type ApiOptions } from "Api";
|
||||||
import type { ServerEnv } from "modules/Controller";
|
|
||||||
|
|
||||||
export type AppPlugin = (app: App) => Promise<void> | void;
|
export type AppPluginConfig = {
|
||||||
|
name: string;
|
||||||
|
schema?: () => MaybePromise<ReturnType<typeof prototypeEm> | void>;
|
||||||
|
beforeBuild?: () => MaybePromise<void>;
|
||||||
|
onBuilt?: () => MaybePromise<void>;
|
||||||
|
onServerInit?: (server: Hono<ServerEnv>) => MaybePromise<void>;
|
||||||
|
onFirstBoot?: () => MaybePromise<void>;
|
||||||
|
};
|
||||||
|
export type AppPlugin = (app: App) => AppPluginConfig;
|
||||||
|
|
||||||
abstract class AppEvent<A = {}> extends Event<{ app: App } & A> {}
|
abstract class AppEvent<A = {}> extends Event<{ app: App } & A> {}
|
||||||
export class AppConfigUpdatedEvent extends AppEvent {
|
export class AppConfigUpdatedEvent extends AppEvent {
|
||||||
@@ -73,9 +82,9 @@ export class App {
|
|||||||
modules: ModuleManager;
|
modules: ModuleManager;
|
||||||
adminController?: AdminController;
|
adminController?: AdminController;
|
||||||
_id: string = crypto.randomUUID();
|
_id: string = crypto.randomUUID();
|
||||||
|
plugins: AppPluginConfig[];
|
||||||
|
|
||||||
private trigger_first_boot = false;
|
private trigger_first_boot = false;
|
||||||
private plugins: AppPlugin[];
|
|
||||||
private _building: boolean = false;
|
private _building: boolean = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -83,13 +92,14 @@ export class App {
|
|||||||
_initialConfig?: InitialModuleConfigs,
|
_initialConfig?: InitialModuleConfigs,
|
||||||
private options?: AppOptions,
|
private options?: AppOptions,
|
||||||
) {
|
) {
|
||||||
this.plugins = options?.plugins ?? [];
|
this.plugins = (options?.plugins ?? []).map((plugin) => plugin(this));
|
||||||
this.modules = new ModuleManager(connection, {
|
this.modules = new ModuleManager(connection, {
|
||||||
...(options?.manager ?? {}),
|
...(options?.manager ?? {}),
|
||||||
initial: _initialConfig,
|
initial: _initialConfig,
|
||||||
onUpdated: this.onUpdated.bind(this),
|
onUpdated: this.onUpdated.bind(this),
|
||||||
onFirstBoot: this.onFirstBoot.bind(this),
|
onFirstBoot: this.onFirstBoot.bind(this),
|
||||||
onServerInit: this.onServerInit.bind(this),
|
onServerInit: this.onServerInit.bind(this),
|
||||||
|
onModulesBuilt: this.onModulesBuilt.bind(this),
|
||||||
});
|
});
|
||||||
this.modules.ctx().emgr.registerEvents(AppEvents);
|
this.modules.ctx().emgr.registerEvents(AppEvents);
|
||||||
}
|
}
|
||||||
@@ -98,6 +108,32 @@ export class App {
|
|||||||
return this.modules.ctx().emgr;
|
return this.modules.ctx().emgr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async runPlugins<Key extends keyof AppPluginConfig>(
|
||||||
|
key: Key,
|
||||||
|
...args: any[]
|
||||||
|
): Promise<{ name: string; result: any }[]> {
|
||||||
|
const results: { name: string; result: any }[] = [];
|
||||||
|
for (const plugin of this.plugins) {
|
||||||
|
try {
|
||||||
|
if (key in plugin && plugin[key]) {
|
||||||
|
const fn = plugin[key];
|
||||||
|
if (fn && typeof fn === "function") {
|
||||||
|
$console.debug(`[Plugin:${plugin.name}] ${key}`);
|
||||||
|
// @ts-expect-error
|
||||||
|
const result = await fn(...args);
|
||||||
|
results.push({
|
||||||
|
name: plugin.name,
|
||||||
|
result,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$console.warn(`[Plugin:${plugin.name}] error running "${key}"`, String(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results as any;
|
||||||
|
}
|
||||||
|
|
||||||
async build(options?: { sync?: boolean; fetch?: boolean; forceBuild?: boolean }) {
|
async build(options?: { sync?: boolean; fetch?: boolean; forceBuild?: boolean }) {
|
||||||
// prevent multiple concurrent builds
|
// prevent multiple concurrent builds
|
||||||
if (this._building) {
|
if (this._building) {
|
||||||
@@ -106,6 +142,8 @@ export class App {
|
|||||||
}
|
}
|
||||||
if (!options?.forceBuild) return;
|
if (!options?.forceBuild) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.runPlugins("beforeBuild");
|
||||||
this._building = true;
|
this._building = true;
|
||||||
|
|
||||||
if (options?.sync) this.modules.ctx().flags.sync_required = true;
|
if (options?.sync) this.modules.ctx().flags.sync_required = true;
|
||||||
@@ -117,13 +155,10 @@ export class App {
|
|||||||
guard.registerPermissions(Object.values(SystemPermissions));
|
guard.registerPermissions(Object.values(SystemPermissions));
|
||||||
server.route("/api/system", new SystemController(this).getController());
|
server.route("/api/system", new SystemController(this).getController());
|
||||||
|
|
||||||
// load plugins
|
// emit built event
|
||||||
if (this.plugins.length > 0) {
|
|
||||||
await Promise.all(this.plugins.map((plugin) => plugin(this)));
|
|
||||||
}
|
|
||||||
|
|
||||||
$console.log("App built");
|
$console.log("App built");
|
||||||
await this.emgr.emit(new AppBuiltEvent({ app: this }));
|
await this.emgr.emit(new AppBuiltEvent({ app: this }));
|
||||||
|
await this.runPlugins("onBuilt");
|
||||||
|
|
||||||
// first boot is set from ModuleManager when there wasn't a config table
|
// first boot is set from ModuleManager when there wasn't a config table
|
||||||
if (this.trigger_first_boot) {
|
if (this.trigger_first_boot) {
|
||||||
@@ -223,12 +258,13 @@ export class App {
|
|||||||
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this }));
|
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this }));
|
||||||
}
|
}
|
||||||
|
|
||||||
async onFirstBoot() {
|
protected async onFirstBoot() {
|
||||||
$console.log("App first boot");
|
$console.log("App first boot");
|
||||||
this.trigger_first_boot = true;
|
this.trigger_first_boot = true;
|
||||||
|
await this.runPlugins("onFirstBoot");
|
||||||
}
|
}
|
||||||
|
|
||||||
async onServerInit(server: Hono<ServerEnv>) {
|
protected async onServerInit(server: Hono<ServerEnv>) {
|
||||||
server.use(async (c, next) => {
|
server.use(async (c, next) => {
|
||||||
c.set("app", this);
|
c.set("app", this);
|
||||||
await this.emgr.emit(new AppRequest({ app: this, request: c.req.raw }));
|
await this.emgr.emit(new AppRequest({ app: this, request: c.req.raw }));
|
||||||
@@ -258,6 +294,23 @@ export class App {
|
|||||||
if (this.options?.manager?.onServerInit) {
|
if (this.options?.manager?.onServerInit) {
|
||||||
this.options.manager.onServerInit(server);
|
this.options.manager.onServerInit(server);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.runPlugins("onServerInit", server);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async onModulesBuilt(ctx: ModuleBuildContext) {
|
||||||
|
const results = (await this.runPlugins("schema")) as {
|
||||||
|
name: string;
|
||||||
|
result: ReturnType<typeof prototypeEm>;
|
||||||
|
}[];
|
||||||
|
if (results.length > 0) {
|
||||||
|
for (const { name, result } of results) {
|
||||||
|
if (result) {
|
||||||
|
$console.log(`[Plugin:${name}] schema`);
|
||||||
|
ctx.helper.ensureSchema(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
|||||||
|
|
||||||
registerEntities() {
|
registerEntities() {
|
||||||
const users = this.getUsersEntity(true);
|
const users = this.getUsersEntity(true);
|
||||||
this.ensureSchema(
|
this.ctx.helper.ensureSchema(
|
||||||
em(
|
em(
|
||||||
{
|
{
|
||||||
[users.name as "users"]: users,
|
[users.name as "users"]: users,
|
||||||
@@ -153,13 +153,13 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const roles = Object.keys(this.config.roles ?? {});
|
const roles = Object.keys(this.config.roles ?? {});
|
||||||
this.replaceEntityField(users, "role", enumm({ enum: roles }));
|
this.ctx.helper.replaceEntityField(users, "role", enumm({ enum: roles }));
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// also keep disabled strategies as a choice
|
// also keep disabled strategies as a choice
|
||||||
const strategies = Object.keys(this.config.strategies ?? {});
|
const strategies = Object.keys(this.config.strategies ?? {});
|
||||||
this.replaceEntityField(users, "strategy", enumm({ enum: strategies }));
|
this.ctx.helper.replaceEntityField(users, "strategy", enumm({ enum: strategies }));
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -184,6 +184,6 @@ export class AuthController extends Controller {
|
|||||||
this.registerStrategyActions(strategy, hono);
|
this.registerStrategyActions(strategy, hono);
|
||||||
}
|
}
|
||||||
|
|
||||||
return hono.all("*", (c) => c.notFound());
|
return hono;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,16 @@ export {
|
|||||||
} from "./object/query/query";
|
} from "./object/query/query";
|
||||||
export { Registry, type Constructor } from "./registry/Registry";
|
export { Registry, type Constructor } from "./registry/Registry";
|
||||||
export { getFlashMessage } from "./server/flash";
|
export { getFlashMessage } from "./server/flash";
|
||||||
export { s, jsc, describeRoute } from "./object/schema";
|
export {
|
||||||
|
s,
|
||||||
|
parse,
|
||||||
|
jsc,
|
||||||
|
describeRoute,
|
||||||
|
schemaToSpec,
|
||||||
|
openAPISpecs,
|
||||||
|
type ParseOptions,
|
||||||
|
InvalidSchemaError,
|
||||||
|
} from "./object/schema";
|
||||||
|
|
||||||
export * from "./console";
|
export * from "./console";
|
||||||
export * from "./events";
|
export * from "./events";
|
||||||
|
|||||||
@@ -2,3 +2,5 @@ export interface Serializable<Class, Json extends object = object> {
|
|||||||
toJSON(): Json;
|
toJSON(): Json;
|
||||||
fromJSON(json: Json): Class;
|
fromJSON(json: Json): Class;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MaybePromise<T> = T | Promise<T>;
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ export class DataController extends Controller {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return hono.all("*", (c) => c.notFound());
|
return hono;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getEntityRoutes() {
|
private getEntityRoutes() {
|
||||||
|
|||||||
@@ -12,6 +12,15 @@ export class RelationAccessor {
|
|||||||
return this._relations;
|
return this._relations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exists(relation: EntityRelation): boolean {
|
||||||
|
return this._relations.some(
|
||||||
|
(r) =>
|
||||||
|
r.source.entity.name === relation.source.entity.name &&
|
||||||
|
r.target.entity.name === relation.target.entity.name &&
|
||||||
|
r.type === relation.type,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Searches for the relations of [entity_name]
|
* Searches for the relations of [entity_name]
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ export {
|
|||||||
type InitialModuleConfigs,
|
type InitialModuleConfigs,
|
||||||
} from "./modules/ModuleManager";
|
} from "./modules/ModuleManager";
|
||||||
|
|
||||||
|
export type { ServerEnv } from "modules/Controller";
|
||||||
|
|
||||||
export * as middlewares from "modules/middlewares";
|
export * as middlewares from "modules/middlewares";
|
||||||
export { registries } from "modules/registries";
|
export { registries } from "modules/registries";
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export class AppMedia extends Module<typeof mediaConfigSchema> {
|
|||||||
this.ctx.server.route(this.basepath, new MediaController(this).getController());
|
this.ctx.server.route(this.basepath, new MediaController(this).getController());
|
||||||
|
|
||||||
const media = this.getMediaEntity(true);
|
const media = this.getMediaEntity(true);
|
||||||
this.ensureSchema(
|
this.ctx.helper.ensureSchema(
|
||||||
em({ [media.name as "media"]: media }, ({ index }, { media }) => {
|
em({ [media.name as "media"]: media }, ({ index }, { media }) => {
|
||||||
index(media).on(["path"], true).on(["reference"]).on(["entity_id"]);
|
index(media).on(["path"], true).on(["reference"]).on(["entity_id"]);
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -297,6 +297,6 @@ export class MediaController extends Controller {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return hono.all("*", (c) => c.notFound());
|
return hono;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,19 +2,10 @@ import type { Guard } from "auth";
|
|||||||
import { type DebugLogger, SchemaObject } from "core";
|
import { type DebugLogger, SchemaObject } from "core";
|
||||||
import type { EventManager } from "core/events";
|
import type { EventManager } from "core/events";
|
||||||
import type { Static, TSchema } from "core/utils";
|
import type { Static, TSchema } from "core/utils";
|
||||||
import {
|
import type { Connection, EntityManager } from "data";
|
||||||
type Connection,
|
|
||||||
type EntityIndex,
|
|
||||||
type EntityManager,
|
|
||||||
type Field,
|
|
||||||
FieldPrototype,
|
|
||||||
make,
|
|
||||||
type em as prototypeEm,
|
|
||||||
} from "data";
|
|
||||||
import { Entity } from "data";
|
|
||||||
import type { Hono } from "hono";
|
import type { Hono } from "hono";
|
||||||
import { isEqual } from "lodash-es";
|
|
||||||
import type { ServerEnv } from "modules/Controller";
|
import type { ServerEnv } from "modules/Controller";
|
||||||
|
import type { ModuleHelper } from "./ModuleHelper";
|
||||||
|
|
||||||
export type ModuleBuildContext = {
|
export type ModuleBuildContext = {
|
||||||
connection: Connection;
|
connection: Connection;
|
||||||
@@ -24,6 +15,7 @@ export type ModuleBuildContext = {
|
|||||||
guard: Guard;
|
guard: Guard;
|
||||||
logger: DebugLogger;
|
logger: DebugLogger;
|
||||||
flags: (typeof Module)["ctx_flags"];
|
flags: (typeof Module)["ctx_flags"];
|
||||||
|
helper: ModuleHelper;
|
||||||
};
|
};
|
||||||
|
|
||||||
export abstract class Module<Schema extends TSchema = TSchema, ConfigSchema = Static<Schema>> {
|
export abstract class Module<Schema extends TSchema = TSchema, ConfigSchema = Static<Schema>> {
|
||||||
@@ -141,80 +133,4 @@ export abstract class Module<Schema extends TSchema = TSchema, ConfigSchema = St
|
|||||||
toJSON(secrets?: boolean): Static<ReturnType<(typeof this)["getSchema"]>> {
|
toJSON(secrets?: boolean): Static<ReturnType<(typeof this)["getSchema"]>> {
|
||||||
return this.config;
|
return this.config;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected ensureEntity(entity: Entity) {
|
|
||||||
const instance = this.ctx.em.entity(entity.name, true);
|
|
||||||
|
|
||||||
// check fields
|
|
||||||
if (!instance) {
|
|
||||||
this.ctx.em.addEntity(entity);
|
|
||||||
this.ctx.flags.sync_required = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if exists, check all fields required are there
|
|
||||||
// @todo: check if the field also equal
|
|
||||||
for (const field of entity.fields) {
|
|
||||||
const instanceField = instance.field(field.name);
|
|
||||||
if (!instanceField) {
|
|
||||||
instance.addField(field);
|
|
||||||
this.ctx.flags.sync_required = true;
|
|
||||||
} else {
|
|
||||||
const changes = this.setEntityFieldConfigs(field, instanceField);
|
|
||||||
if (changes > 0) {
|
|
||||||
this.ctx.flags.sync_required = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// replace entity (mainly to keep the ensured type)
|
|
||||||
this.ctx.em.__replaceEntity(
|
|
||||||
new Entity(instance.name, instance.fields, instance.config, entity.type),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected ensureIndex(index: EntityIndex) {
|
|
||||||
if (!this.ctx.em.hasIndex(index)) {
|
|
||||||
this.ctx.em.addIndex(index);
|
|
||||||
this.ctx.flags.sync_required = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected ensureSchema<Schema extends ReturnType<typeof prototypeEm>>(schema: Schema): Schema {
|
|
||||||
Object.values(schema.entities ?? {}).forEach(this.ensureEntity.bind(this));
|
|
||||||
schema.indices?.forEach(this.ensureIndex.bind(this));
|
|
||||||
|
|
||||||
return schema;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected setEntityFieldConfigs(
|
|
||||||
parent: Field,
|
|
||||||
child: Field,
|
|
||||||
props: string[] = ["hidden", "fillable", "required"],
|
|
||||||
) {
|
|
||||||
let changes = 0;
|
|
||||||
for (const prop of props) {
|
|
||||||
if (!isEqual(child.config[prop], parent.config[prop])) {
|
|
||||||
child.config[prop] = parent.config[prop];
|
|
||||||
changes++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return changes;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected replaceEntityField(
|
|
||||||
_entity: string | Entity,
|
|
||||||
field: Field | string,
|
|
||||||
_newField: Field | FieldPrototype,
|
|
||||||
) {
|
|
||||||
const entity = this.ctx.em.entity(_entity);
|
|
||||||
const name = typeof field === "string" ? field : field.name;
|
|
||||||
const newField =
|
|
||||||
_newField instanceof FieldPrototype ? make(name, _newField as any) : _newField;
|
|
||||||
|
|
||||||
// ensure keeping vital config
|
|
||||||
this.setEntityFieldConfigs(entity.field(name)!, newField);
|
|
||||||
|
|
||||||
entity.__replaceField(name, newField);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
113
app/src/modules/ModuleHelper.ts
Normal file
113
app/src/modules/ModuleHelper.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import {
|
||||||
|
type EntityIndex,
|
||||||
|
type EntityRelation,
|
||||||
|
type Field,
|
||||||
|
type em as prototypeEm,
|
||||||
|
FieldPrototype,
|
||||||
|
make,
|
||||||
|
Entity,
|
||||||
|
entityTypes,
|
||||||
|
} from "data";
|
||||||
|
import { isEqual } from "lodash-es";
|
||||||
|
import type { ModuleBuildContext } from "./Module";
|
||||||
|
|
||||||
|
export class ModuleHelper {
|
||||||
|
constructor(protected ctx: Omit<ModuleBuildContext, "helper">) {}
|
||||||
|
|
||||||
|
get em() {
|
||||||
|
return this.ctx.em;
|
||||||
|
}
|
||||||
|
|
||||||
|
get flags() {
|
||||||
|
return this.ctx.flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureEntity(entity: Entity) {
|
||||||
|
const instance = this.em.entity(entity.name, true);
|
||||||
|
|
||||||
|
// check fields
|
||||||
|
if (!instance) {
|
||||||
|
this.em.addEntity(entity);
|
||||||
|
this.flags.sync_required = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if exists, check all fields required are there
|
||||||
|
// @todo: potentially identify system and generated entities and take that as instance
|
||||||
|
// @todo: check if the field also equal
|
||||||
|
for (const field of entity.fields) {
|
||||||
|
const instanceField = instance.field(field.name);
|
||||||
|
if (!instanceField) {
|
||||||
|
instance.addField(field);
|
||||||
|
this.flags.sync_required = true;
|
||||||
|
} else {
|
||||||
|
const changes = this.setEntityFieldConfigs(field, instanceField);
|
||||||
|
if (changes > 0) {
|
||||||
|
this.flags.sync_required = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if type is different, keep the highest
|
||||||
|
if (instance.type !== entity.type) {
|
||||||
|
const instance_i = entityTypes.indexOf(instance.type);
|
||||||
|
const entity_i = entityTypes.indexOf(entity.type);
|
||||||
|
const type = entity_i > instance_i ? entity.type : instance.type;
|
||||||
|
|
||||||
|
this.em.__replaceEntity(new Entity(instance.name, instance.fields, instance.config, type));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureIndex(index: EntityIndex) {
|
||||||
|
if (!this.em.hasIndex(index)) {
|
||||||
|
this.em.addIndex(index);
|
||||||
|
this.flags.sync_required = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureRelation(relation: EntityRelation) {
|
||||||
|
if (!this.em.relations.exists(relation)) {
|
||||||
|
this.em.addRelation(relation);
|
||||||
|
this.flags.sync_required = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureSchema<Schema extends ReturnType<typeof prototypeEm>>(schema: Schema): Schema {
|
||||||
|
Object.values(schema.entities ?? {}).forEach(this.ensureEntity.bind(this));
|
||||||
|
schema.indices?.forEach(this.ensureIndex.bind(this));
|
||||||
|
schema.relations?.forEach(this.ensureRelation.bind(this));
|
||||||
|
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
setEntityFieldConfigs(
|
||||||
|
parent: Field,
|
||||||
|
child: Field,
|
||||||
|
props: string[] = ["hidden", "fillable", "required"],
|
||||||
|
) {
|
||||||
|
let changes = 0;
|
||||||
|
for (const prop of props) {
|
||||||
|
if (!isEqual(child.config[prop], parent.config[prop])) {
|
||||||
|
child.config[prop] = parent.config[prop];
|
||||||
|
changes++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceEntityField(
|
||||||
|
_entity: string | Entity,
|
||||||
|
field: Field | string,
|
||||||
|
_newField: Field | FieldPrototype,
|
||||||
|
) {
|
||||||
|
const entity = this.em.entity(_entity);
|
||||||
|
const name = typeof field === "string" ? field : field.name;
|
||||||
|
const newField =
|
||||||
|
_newField instanceof FieldPrototype ? make(name, _newField as any) : _newField;
|
||||||
|
|
||||||
|
// ensure keeping vital config
|
||||||
|
this.setEntityFieldConfigs(entity.field(name)!, newField);
|
||||||
|
|
||||||
|
entity.__replaceField(name, newField);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,7 @@ import { AppMedia } from "../media/AppMedia";
|
|||||||
import type { ServerEnv } from "./Controller";
|
import type { ServerEnv } from "./Controller";
|
||||||
import { Module, type ModuleBuildContext } from "./Module";
|
import { Module, type ModuleBuildContext } from "./Module";
|
||||||
import * as tbbox from "@sinclair/typebox";
|
import * as tbbox from "@sinclair/typebox";
|
||||||
|
import { ModuleHelper } from "./ModuleHelper";
|
||||||
const { Type } = tbbox;
|
const { Type } = tbbox;
|
||||||
|
|
||||||
export type { ModuleBuildContext };
|
export type { ModuleBuildContext };
|
||||||
@@ -92,6 +93,8 @@ export type ModuleManagerOptions = {
|
|||||||
trustFetched?: boolean;
|
trustFetched?: boolean;
|
||||||
// runs when initial config provided on a fresh database
|
// runs when initial config provided on a fresh database
|
||||||
seed?: (ctx: ModuleBuildContext) => Promise<void>;
|
seed?: (ctx: ModuleBuildContext) => Promise<void>;
|
||||||
|
// called right after modules are built, before finish
|
||||||
|
onModulesBuilt?: (ctx: ModuleBuildContext) => Promise<void>;
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
verbosity?: Verbosity;
|
verbosity?: Verbosity;
|
||||||
};
|
};
|
||||||
@@ -267,7 +270,7 @@ export class ModuleManager {
|
|||||||
this.guard = new Guard();
|
this.guard = new Guard();
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const ctx = {
|
||||||
connection: this.connection,
|
connection: this.connection,
|
||||||
server: this.server,
|
server: this.server,
|
||||||
em: this.em,
|
em: this.em,
|
||||||
@@ -276,6 +279,11 @@ export class ModuleManager {
|
|||||||
flags: Module.ctx_flags,
|
flags: Module.ctx_flags,
|
||||||
logger: this.logger,
|
logger: this.logger,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...ctx,
|
||||||
|
helper: new ModuleHelper(ctx),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetch(): Promise<ConfigTable | undefined> {
|
private async fetch(): Promise<ConfigTable | undefined> {
|
||||||
@@ -549,6 +557,10 @@ export class ModuleManager {
|
|||||||
this._built = state.built = true;
|
this._built = state.built = true;
|
||||||
this.logger.log("modules built", ctx.flags);
|
this.logger.log("modules built", ctx.flags);
|
||||||
|
|
||||||
|
if (this.options?.onModulesBuilt) {
|
||||||
|
await this.options.onModulesBuilt(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
if (options?.ignoreFlags !== true) {
|
if (options?.ignoreFlags !== true) {
|
||||||
if (ctx.flags.sync_required) {
|
if (ctx.flags.sync_required) {
|
||||||
ctx.flags.sync_required = false;
|
ctx.flags.sync_required = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user