You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
175 lines
5.0 KiB
175 lines
5.0 KiB
// lib/core/component-registry.ts
|
|
// Copyright (C) 2025 DTP Technologies, LLC
|
|
// All Rights Reserved
|
|
|
|
import env from "../../config/env.js";
|
|
import assert from "node:assert";
|
|
|
|
import { promises as fs } from "node:fs";
|
|
import path from "node:path";
|
|
|
|
import mongoose from "mongoose";
|
|
|
|
import { DtpBase } from "./base.js";
|
|
import { DtpPlatform } from "./platform.js";
|
|
import { DtpService } from "./service.js";
|
|
import { WebServer } from "../web/server.js";
|
|
import { WebController } from "../web/controller.js";
|
|
|
|
type ModelRegistry = Record<string, typeof mongoose.Model>;
|
|
type ServiceRegistry = Record<string, DtpService>;
|
|
type ControllerRegistry = Record<string, WebController>;
|
|
|
|
export class DtpComponentRegistry extends DtpBase {
|
|
models: ModelRegistry = {};
|
|
services: ServiceRegistry = {};
|
|
controllers: ControllerRegistry = {};
|
|
|
|
static get name() {
|
|
return "DtpComponentRegistry";
|
|
}
|
|
|
|
static get slug() {
|
|
return "DtpComponentRegistry";
|
|
}
|
|
|
|
constructor(platform: DtpPlatform) {
|
|
super(platform, DtpComponentRegistry);
|
|
}
|
|
|
|
async loadModels() {
|
|
const basePath = path.join(env.src, "app", "models");
|
|
this.log.info("loading models", { basePath });
|
|
|
|
const entries = await fs.readdir(basePath, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
if (!entry.isFile()) {
|
|
continue;
|
|
}
|
|
if (!entry.name.endsWith("js") && !entry.name.endsWith("ts")) {
|
|
continue;
|
|
}
|
|
|
|
const filename = path.join(basePath, entry.name);
|
|
const model = (await import(filename)).default;
|
|
if (this.models[model.modelName]) {
|
|
this.log.error("model name collision", { name: model.modelName });
|
|
throw new Error(
|
|
"You are registering more than one model with the same name"
|
|
);
|
|
}
|
|
this.models[model.modelName] = model;
|
|
this.log.info("model loaded", { name: model.modelName });
|
|
}
|
|
}
|
|
|
|
async loadServices(): Promise<void> {
|
|
const basePath = path.join(env.src, "app", "services");
|
|
this.log.debug("loading services", { basePath });
|
|
|
|
const entries = await fs.readdir(basePath, { withFileTypes: true });
|
|
const inits = [];
|
|
|
|
for await (const entry of entries) {
|
|
if (!entry.isFile()) {
|
|
continue;
|
|
}
|
|
if (!entry.name.endsWith("js") && !entry.name.endsWith("ts")) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
const ServiceClass = (await import(path.join(basePath, entry.name)))
|
|
.default;
|
|
|
|
this.log.info("loading service", {
|
|
script: entry.name,
|
|
name: ServiceClass.name,
|
|
slug: ServiceClass.slug,
|
|
});
|
|
|
|
const service = new ServiceClass(this.platform);
|
|
|
|
this.services[ServiceClass.slug] = service;
|
|
inits.push(service);
|
|
} catch (error) {
|
|
this.log.error("failed to load service", { script: entry, error });
|
|
throw new Error("failed to load service", { cause: error });
|
|
}
|
|
}
|
|
|
|
for await (const service of inits) {
|
|
await service.start();
|
|
}
|
|
}
|
|
|
|
getService<T>(slug: string): T {
|
|
const service = this.services[slug];
|
|
if (!service) {
|
|
throw new Error(`Service ${slug} is not loaded`);
|
|
}
|
|
return service as T;
|
|
}
|
|
|
|
async loadControllers(server: WebServer): Promise<void> {
|
|
assert(server.app, "ExpressJS instance required");
|
|
|
|
const basePath = path.join(env.src, "app", "controllers");
|
|
this.log.debug("loading controllers", { basePath });
|
|
|
|
const entries = await fs.readdir(basePath, { withFileTypes: true });
|
|
const inits = [];
|
|
|
|
for await (const entry of entries) {
|
|
if (!entry.isFile()) {
|
|
continue;
|
|
}
|
|
if (!entry.name.endsWith("js") && !entry.name.endsWith("ts")) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
const ControllerClass = (await import(path.join(basePath, entry.name)))
|
|
.default;
|
|
if (!ControllerClass) {
|
|
this.log.error("failed to receive a default export from controller", {
|
|
script: entry.name,
|
|
});
|
|
throw new Error("controller failed to provide a default export");
|
|
}
|
|
this.log.info("loading controller", {
|
|
script: entry.name,
|
|
name: ControllerClass.name,
|
|
slug: ControllerClass.slug,
|
|
});
|
|
const controller = new ControllerClass(server, ControllerClass);
|
|
|
|
this.controllers[ControllerClass.slug] = controller;
|
|
inits.push({ ControllerClass, controller });
|
|
} catch (error) {
|
|
this.log.error("failed to load controller", { error });
|
|
throw new Error("failed to load controller", { cause: error });
|
|
}
|
|
}
|
|
|
|
for await (const init of inits) {
|
|
await init.controller.start();
|
|
}
|
|
|
|
for await (const init of inits) {
|
|
this.log.info("mounting controller", {
|
|
controller: init.ControllerClass.name,
|
|
route: init.controller.route,
|
|
});
|
|
server.app.use(init.controller.route, init.controller.router);
|
|
}
|
|
}
|
|
|
|
getController<T>(slug: string): T {
|
|
const controller = this.controllers[slug];
|
|
if (!controller) {
|
|
throw new Error(`Controller ${slug} is not loaded`);
|
|
}
|
|
return controller as T;
|
|
}
|
|
}
|
|
|