feat: improved abilities of plugins, moved schema fns to ctx

This commit is contained in:
dswbx
2025-06-12 15:29:53 +02:00
parent 9c4aac8843
commit 8b4b63b3cd
17 changed files with 330 additions and 133 deletions

View File

@@ -1,7 +1,7 @@
import type { CreateUserPayload } from "auth/AppAuth";
import { $console } from "core";
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 {
ModuleManager,
@@ -14,12 +14,21 @@ import {
import * as SystemPermissions from "modules/permissions";
import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
import { SystemController } from "modules/server/SystemController";
import type { MaybePromise } from "core/types";
import type { ServerEnv } from "modules/Controller";
// biome-ignore format: must be here
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> {}
export class AppConfigUpdatedEvent extends AppEvent {
@@ -73,9 +82,9 @@ export class App {
modules: ModuleManager;
adminController?: AdminController;
_id: string = crypto.randomUUID();
plugins: AppPluginConfig[];
private trigger_first_boot = false;
private plugins: AppPlugin[];
private _building: boolean = false;
constructor(
@@ -83,13 +92,14 @@ export class App {
_initialConfig?: InitialModuleConfigs,
private options?: AppOptions,
) {
this.plugins = options?.plugins ?? [];
this.plugins = (options?.plugins ?? []).map((plugin) => plugin(this));
this.modules = new ModuleManager(connection, {
...(options?.manager ?? {}),
initial: _initialConfig,
onUpdated: this.onUpdated.bind(this),
onFirstBoot: this.onFirstBoot.bind(this),
onServerInit: this.onServerInit.bind(this),
onModulesBuilt: this.onModulesBuilt.bind(this),
});
this.modules.ctx().emgr.registerEvents(AppEvents);
}
@@ -98,6 +108,32 @@ export class App {
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 }) {
// prevent multiple concurrent builds
if (this._building) {
@@ -106,6 +142,8 @@ export class App {
}
if (!options?.forceBuild) return;
}
await this.runPlugins("beforeBuild");
this._building = true;
if (options?.sync) this.modules.ctx().flags.sync_required = true;
@@ -117,13 +155,10 @@ export class App {
guard.registerPermissions(Object.values(SystemPermissions));
server.route("/api/system", new SystemController(this).getController());
// load plugins
if (this.plugins.length > 0) {
await Promise.all(this.plugins.map((plugin) => plugin(this)));
}
// emit built event
$console.log("App built");
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
if (this.trigger_first_boot) {
@@ -223,12 +258,13 @@ export class App {
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this }));
}
async onFirstBoot() {
protected async onFirstBoot() {
$console.log("App first boot");
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) => {
c.set("app", this);
await this.emgr.emit(new AppRequest({ app: this, request: c.req.raw }));
@@ -258,6 +294,23 @@ export class App {
if (this.options?.manager?.onServerInit) {
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);
}
}
}
}
}