Files
bknd/app/src/modules/Module.ts
2025-08-14 10:05:15 +02:00

149 lines
4.0 KiB
TypeScript

import type { App } from "bknd";
import type { EventManager } from "core/events";
import type { Connection } from "data/connection";
import type { EntityManager } from "data/entities";
import type { Hono } from "hono";
import type { ServerEnv } from "modules/Controller";
import type { ModuleHelper } from "./ModuleHelper";
import { SchemaObject } from "core/object/SchemaObject";
import type { Guard } from "auth/authorize/Guard";
import type { McpServer, DebugLogger } from "bknd/utils";
type PartialRec<T> = { [P in keyof T]?: PartialRec<T[P]> };
export type ModuleBuildContextMcpContext = {
app: App;
ctx: () => ModuleBuildContext;
};
export type ModuleBuildContext = {
connection: Connection;
server: Hono<ServerEnv>;
em: EntityManager;
emgr: EventManager<any>;
guard: Guard;
logger: DebugLogger;
flags: (typeof Module)["ctx_flags"];
helper: ModuleHelper;
mcp: McpServer<ModuleBuildContextMcpContext>;
};
export abstract class Module<Schema extends object = object> {
private _built = false;
private _schema: SchemaObject<ReturnType<(typeof this)["getSchema"]>>;
private _listener: any = () => null;
constructor(
initial?: PartialRec<Schema>,
protected _ctx?: ModuleBuildContext,
) {
this._schema = new SchemaObject(this.getSchema(), initial, {
forceParse: this.useForceParse(),
onUpdate: async (c) => {
await this._listener(c);
},
restrictPaths: this.getRestrictedPaths(),
overwritePaths: this.getOverwritePaths(),
onBeforeUpdate: this.onBeforeUpdate.bind(this),
});
}
static ctx_flags = {
sync_required: false,
ctx_reload_required: false,
} as {
// signal that a sync is required at the end of build
sync_required: boolean;
ctx_reload_required: boolean;
};
onBeforeUpdate(from: Schema, to: Schema): Schema | Promise<Schema> {
return to;
}
setListener(listener: (c: ReturnType<(typeof this)["getSchema"]>) => void | Promise<void>) {
this._listener = listener;
return this;
}
// @todo: test all getSchema() for additional properties
abstract getSchema();
useForceParse() {
return false;
}
getRestrictedPaths(): string[] | undefined {
return undefined;
}
/**
* These paths will be overwritten, even when "patch" is called.
* This is helpful if there are keys that contains records, which always be sent in full.
*/
getOverwritePaths(): (RegExp | string)[] | undefined {
return undefined;
}
//get configDefault(): s.Static<ReturnType<(typeof this)["getSchema"]>> {
get configDefault(): Schema {
return this._schema.default() as any;
}
//get config(): s.Static<ReturnType<(typeof this)["getSchema"]>> {
get config(): Schema {
return this._schema.get();
}
setContext(ctx: ModuleBuildContext) {
this._ctx = ctx;
return this;
}
schema() {
return this._schema;
}
// action performed when server has been initialized
// can be used to assign global middlewares
onServerInit(hono: Hono<ServerEnv>) {}
get ctx() {
if (!this._ctx) {
throw new Error("Context not set");
}
return this._ctx;
}
async build() {
throw new Error("Not implemented");
}
setBuilt() {
this._built = true;
this._schema = new SchemaObject(this.getSchema(), this.toJSON(true), {
onUpdate: async (c) => {
await this._listener(c);
},
forceParse: this.useForceParse(),
restrictPaths: this.getRestrictedPaths(),
overwritePaths: this.getOverwritePaths(),
onBeforeUpdate: this.onBeforeUpdate.bind(this),
});
}
isBuilt() {
return this._built;
}
throwIfNotBuilt() {
if (!this._built) {
throw new Error("Config not built: " + this.constructor.name);
}
}
//toJSON(secrets?: boolean): s.Static<ReturnType<(typeof this)["getSchema"]>> {
toJSON(secrets?: boolean): Schema {
return this.config;
}
}