A virtual newsroom powered by RSS and AI.
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

// 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;
}
}