diff --git a/packages/architectura/package.json b/packages/architectura/package.json index 49e92394..2c114651 100755 --- a/packages/architectura/package.json +++ b/packages/architectura/package.json @@ -1,6 +1,6 @@ { "name": "@vitruvius-labs/architectura", - "version": "0.9.0", + "version": "0.10.0", "description": "A light weight strongly typed Node.JS framework providing isolated context for each request.", "author": { "name": "VitruviusLabs" diff --git a/packages/architectura/src/core/endpoint/_index.mts b/packages/architectura/src/core/endpoint/_index.mts index 7baebc67..8bafb5bc 100644 --- a/packages/architectura/src/core/endpoint/_index.mts +++ b/packages/architectura/src/core/endpoint/_index.mts @@ -1,2 +1,4 @@ +export * from "./definition/_index.mjs"; export * from "./base.endpoint.mjs"; export * from "./endpoint.registry.mjs"; +export * from "./route.utility.mjs"; diff --git a/packages/architectura/src/core/endpoint/base.endpoint.mts b/packages/architectura/src/core/endpoint/base.endpoint.mts index 81b2b0ae..3a53230d 100644 --- a/packages/architectura/src/core/endpoint/base.endpoint.mts +++ b/packages/architectura/src/core/endpoint/base.endpoint.mts @@ -3,9 +3,9 @@ import type { BasePreHook } from "../hook/base.pre-hook.mjs"; import type { BasePostHook } from "../hook/base.post-hook.mjs"; import type { BaseErrorHook } from "../hook/base.error-hook.mjs"; import type { ExecutionContext } from "../execution-context/execution-context.mjs"; -import { Singleton } from "../../utility/singleton.mjs"; +import { RouteUtility } from "./route.utility.mjs"; -abstract class BaseEndpoint extends Singleton +abstract class BaseEndpoint { protected abstract readonly method: HTTPMethodEnum; protected abstract readonly route: RegExp | string; @@ -15,34 +15,6 @@ abstract class BaseEndpoint extends Singleton protected readonly excludedGlobalPostHooks: Array = []; protected readonly errorHooks: Array = []; protected readonly excludedGlobalErrorHooks: Array = []; - private normalizedRoute: RegExp | undefined = undefined; - - private static NormalizeRoute(route: RegExp | string): RegExp - { - if (route instanceof RegExp) - { - return new RegExp(BaseEndpoint.MakeRouteWhole(route.source), route.flags); - } - - return new RegExp(BaseEndpoint.MakeRouteWhole(route)); - } - - private static MakeRouteWhole(route: string): string - { - let pattern: string = route; - - if (!route.startsWith("^")) - { - pattern = `^${pattern}`; - } - - if (!route.endsWith("$")) - { - pattern = `${pattern}$`; - } - - return pattern; - } public abstract execute(context: ExecutionContext): Promise | void; @@ -55,12 +27,7 @@ abstract class BaseEndpoint extends Singleton /** @sealed */ public getRoute(): RegExp { - if (this.normalizedRoute === undefined) - { - this.normalizedRoute = BaseEndpoint.NormalizeRoute(this.route); - } - - return this.normalizedRoute; + return RouteUtility.NormalizeRoute(this.route); } public getPreHooks(): Array diff --git a/packages/architectura/src/core/endpoint/definition/_index.mts b/packages/architectura/src/core/endpoint/definition/_index.mts new file mode 100644 index 00000000..9b9b0ad4 --- /dev/null +++ b/packages/architectura/src/core/endpoint/definition/_index.mts @@ -0,0 +1 @@ +export * from "./interface/endpoint-entry.interface.mjs"; diff --git a/packages/architectura/src/core/endpoint/definition/interface/endpoint-entry.interface.mts b/packages/architectura/src/core/endpoint/definition/interface/endpoint-entry.interface.mts new file mode 100644 index 00000000..3407085e --- /dev/null +++ b/packages/architectura/src/core/endpoint/definition/interface/endpoint-entry.interface.mts @@ -0,0 +1,13 @@ +import type { ConstructorOf } from "@vitruvius-labs/ts-predicate"; +import type { HTTPMethodEnum } from "../../../definition/enum/http-method.enum.mjs"; +import type { BaseEndpoint } from "../../base.endpoint.mjs"; + +/** @internal */ +interface EndpointEntryInterface +{ + method: HTTPMethodEnum; + route: RegExp; + endpoint: BaseEndpoint | ConstructorOf; +} + +export type { EndpointEntryInterface }; diff --git a/packages/architectura/src/core/endpoint/endpoint.registry.mts b/packages/architectura/src/core/endpoint/endpoint.registry.mts index fd3e1bd3..afc81d1f 100644 --- a/packages/architectura/src/core/endpoint/endpoint.registry.mts +++ b/packages/architectura/src/core/endpoint/endpoint.registry.mts @@ -1,55 +1,85 @@ import type { Dirent } from "node:fs"; -import { isFunction, isRecord, isString } from "@vitruvius-labs/ts-predicate/type-guard"; +import type { HTTPMethodEnum } from "../definition/enum/http-method.enum.mjs"; +import type { EndpointEntryInterface } from "./definition/interface/endpoint-entry.interface.mjs"; +import { isFunction, isRecord } from "@vitruvius-labs/ts-predicate/type-guard"; import { type ConstructorOf, getConstructorOf } from "@vitruvius-labs/ts-predicate/helper"; import { HelloWorldEndpoint } from "../../endpoint/hello-world.endpoint.mjs"; import { FileSystemService } from "../../service/file-system/file-system.service.mjs"; import { LoggerProxy } from "../../service/logger/logger.proxy.mjs"; import { BaseEndpoint } from "./base.endpoint.mjs"; -import { isHTTPMethodEnum } from "../predicate/is-http-method-enum.mjs"; -import { Singleton } from "../../utility/singleton.mjs"; class EndpointRegistry { - private static readonly ENDPOINTS: Map = new Map(); + private static readonly ENDPOINTS: Map = new Map(); - public static GetEndpoints(): ReadonlyMap + public static FindEndpoint(request_method: HTTPMethodEnum, request_path: string): BaseEndpoint | undefined { if (this.ENDPOINTS.size === 0) { - LoggerProxy.Warning("No endpoints have been added. Default endpoint."); + LoggerProxy.Warning("No endpoint have been added. Default endpoint."); - const MAP: Map = new Map(); - - let endpoint: HelloWorldEndpoint | undefined = Singleton.FindInstance(HelloWorldEndpoint); + return new HelloWorldEndpoint(); + } - if (endpoint === undefined) + for (const [, ENDPOINT_ENTRY] of this.ENDPOINTS) + { + if (ENDPOINT_ENTRY.method === request_method && ENDPOINT_ENTRY.route.test(request_path)) { - endpoint = new HelloWorldEndpoint(); - } - - MAP.set("GET::/^.*$/", endpoint); + if (ENDPOINT_ENTRY.endpoint instanceof BaseEndpoint) + { + return ENDPOINT_ENTRY.endpoint; + } - return MAP; + return new ENDPOINT_ENTRY.endpoint(); + } } - return this.ENDPOINTS; + return undefined; } - public static AddEndpoint(endpoint: BaseEndpoint): void + public static AddEndpoint(endpoint: BaseEndpoint | ConstructorOf): void { - const METHOD: string = endpoint.getMethod(); - const ROUTE: string = endpoint.getRoute().toString(); + let constructor_class: ConstructorOf | undefined = undefined; + let instance: BaseEndpoint | undefined = undefined; + + if (endpoint instanceof BaseEndpoint) + { + constructor_class = getConstructorOf(endpoint); + instance = endpoint; + } + else + { + constructor_class = endpoint; + instance = new endpoint(); + } + + const METHOD: HTTPMethodEnum = instance.getMethod(); + const ROUTE: RegExp = instance.getRoute(); + + const IDENTIFIER: string = `${METHOD}::${ROUTE.toString()}`; - const IDENTIFIER: string = `${METHOD}::${ROUTE}`; + const ENTRY: EndpointEntryInterface | undefined = this.ENDPOINTS.get(IDENTIFIER); - if (this.ENDPOINTS.has(IDENTIFIER)) + if (ENTRY !== undefined) { - throw new Error(`An endpoint is already added for method ${METHOD} and route "${ROUTE}".`); + if (ENTRY.endpoint === endpoint) + { + throw new Error(`Endpoint ${constructor_class.name} already added.`); + } + + throw new Error(`An endpoint is already added for method ${METHOD} and route "${ROUTE.toString()}".`); } - LoggerProxy.Debug(`Endpoint added ${METHOD} ${ROUTE}.`); + LoggerProxy.Debug(`Endpoint added ${METHOD} ${ROUTE.toString()}.`); - this.ENDPOINTS.set(IDENTIFIER, endpoint); + this.ENDPOINTS.set( + IDENTIFIER, + { + method: METHOD, + route: ROUTE, + endpoint: endpoint, + } + ); } public static async AddEndpointsDirectory(directory: string): Promise @@ -65,18 +95,18 @@ class EndpointRegistry for (const ENTITY of ENTITIES) { - const FILE_PATH: string = `${directory}/${ENTITY.name}`; + const ENTITY_PATH: string = `${directory}/${ENTITY.name}`; if (ENTITY.isDirectory()) { - await this.ParseDirectoryForEndpoints(FILE_PATH); + await this.ParseDirectoryForEndpoints(ENTITY_PATH); continue; } if (ENTITY.isFile() && ENTITY.name.includes(".endpoint.")) { - await this.ExtractEndpoint(FILE_PATH); + await this.ExtractEndpoint(ENTITY_PATH); } } } @@ -87,16 +117,11 @@ class EndpointRegistry if (isRecord(EXPORTS)) { - for (let [, endpoint] of Object.entries(EXPORTS)) + for (const [, EXPORT] of Object.entries(EXPORTS)) { - if (this.IsEndpointConstructor(endpoint)) + if (this.IsEndpoint(EXPORT)) { - endpoint = new endpoint(); - } - - if (this.IsConcreteEndpoint(endpoint)) - { - this.AddEndpoint(endpoint); + this.AddEndpoint(EXPORT); return; } @@ -104,38 +129,9 @@ class EndpointRegistry } } - private static IsEndpointConstructor(value: unknown): value is ConstructorOf - { - return isFunction(value) && value.prototype instanceof BaseEndpoint; - } - - private static IsConcreteEndpoint(value: unknown): value is BaseEndpoint + private static IsEndpoint(value: unknown): value is BaseEndpoint | ConstructorOf { - if (!(value instanceof BaseEndpoint)) - { - return false; - } - - const method: unknown = Reflect.get(value, "method"); - const route: unknown = Reflect.get(value, "route"); - - if (method === undefined && route === undefined) - { - // Abstract class - return false; - } - - if (!isHTTPMethodEnum(method)) - { - throw new Error(`${getConstructorOf(value).name} method must be an HTTPMethodEnum.`); - } - - if (!isString(route) && !(route instanceof RegExp)) - { - throw new Error(`${getConstructorOf(value).name} route must be a string or RegExp.`); - } - - return true; + return value instanceof BaseEndpoint || isFunction(value) && value.prototype instanceof BaseEndpoint; } } diff --git a/packages/architectura/src/core/endpoint/route.utility.mts b/packages/architectura/src/core/endpoint/route.utility.mts new file mode 100644 index 00000000..6b3dcffd --- /dev/null +++ b/packages/architectura/src/core/endpoint/route.utility.mts @@ -0,0 +1,31 @@ +class RouteUtility +{ + public static NormalizeRoute(route: RegExp | string): RegExp + { + if (route instanceof RegExp) + { + return new RegExp(this.MakeRouteWhole(route.source), route.flags); + } + + return new RegExp(this.MakeRouteWhole(route)); + } + + private static MakeRouteWhole(route: string): string + { + let pattern: string = route; + + if (!route.startsWith("^")) + { + pattern = `^${pattern}`; + } + + if (!route.endsWith("$")) + { + pattern = `${pattern}$`; + } + + return pattern; + } +} + +export { RouteUtility }; diff --git a/packages/architectura/src/core/execution-context/execution-context.registry.mts b/packages/architectura/src/core/execution-context/execution-context.registry.mts index 2325ad82..2bcfc0e6 100644 --- a/packages/architectura/src/core/execution-context/execution-context.registry.mts +++ b/packages/architectura/src/core/execution-context/execution-context.registry.mts @@ -5,19 +5,14 @@ abstract class ExecutionContextRegistry { private static readonly ContextAccessor: AsyncLocalStorage = new AsyncLocalStorage(); - public static GetContextAccessor(): AsyncLocalStorage - { - return this.ContextAccessor; - } - public static GetUnsafeExecutionContext(): ExecutionContext | undefined { - return this.GetContextAccessor().getStore(); + return this.ContextAccessor.getStore(); } public static GetExecutionContext(): ExecutionContext { - const CONTEXT: ExecutionContext | undefined = this.ContextAccessor.getStore(); + const CONTEXT: ExecutionContext | undefined = this.GetUnsafeExecutionContext(); if (CONTEXT === undefined) { @@ -29,7 +24,7 @@ abstract class ExecutionContextRegistry public static SetExecutionContext(execution_context: ExecutionContext): void { - this.GetContextAccessor().enterWith(execution_context); + this.ContextAccessor.enterWith(execution_context); } } diff --git a/packages/architectura/src/core/hook/_index.mts b/packages/architectura/src/core/hook/_index.mts index 1cb76d8b..55a60ffe 100644 --- a/packages/architectura/src/core/hook/_index.mts +++ b/packages/architectura/src/core/hook/_index.mts @@ -1,3 +1,4 @@ export * from "./base.pre-hook.mjs"; export * from "./base.post-hook.mjs"; export * from "./base.error-hook.mjs"; +export * from "./hook.registry.mjs"; diff --git a/packages/architectura/src/core/hook/hook.registry.mts b/packages/architectura/src/core/hook/hook.registry.mts new file mode 100644 index 00000000..b23bfbb2 --- /dev/null +++ b/packages/architectura/src/core/hook/hook.registry.mts @@ -0,0 +1,160 @@ +import type { Dirent } from "node:fs"; +import { type ConstructorOf, getConstructorOf } from "@vitruvius-labs/ts-predicate/helper"; +import { isFunction, isRecord } from "@vitruvius-labs/ts-predicate/type-guard"; +import { FileSystemService } from "../../service/file-system/file-system.service.mjs"; +import { BasePreHook } from "./base.pre-hook.mjs"; +import { BasePostHook } from "./base.post-hook.mjs"; +import { BaseErrorHook } from "./base.error-hook.mjs"; + +class HookRegistry +{ + private static readonly PRE_HOOKS: Array> = []; + private static readonly POST_HOOKS: Array> = []; + private static readonly ERROR_HOOKS: Array> = []; + + /** @internal */ + public static GetPreHooks(): ReadonlyArray + { + return this.PRE_HOOKS.map(this.Instantiate); + } + + public static AddPreHook(hook: BasePreHook | ConstructorOf): void + { + if (this.PRE_HOOKS.includes(hook)) + { + throw new Error(`Pre hook ${this.GetConstructorName(hook)} already added.`); + } + + this.PRE_HOOKS.push(hook); + } + + /** @internal */ + public static GetPostHooks(): ReadonlyArray + { + return this.POST_HOOKS.map(this.Instantiate); + } + + public static AddPostHook(hook: BasePostHook | ConstructorOf): void + { + if (this.POST_HOOKS.includes(hook)) + { + throw new Error(`Post hook ${this.GetConstructorName(hook)} already added.`); + } + + this.POST_HOOKS.push(hook); + } + + /** @internal */ + public static GetErrorHooks(): ReadonlyArray + { + return this.ERROR_HOOKS.map(this.Instantiate); + } + + public static AddErrorHook(hook: BaseErrorHook | ConstructorOf): void + { + if (this.ERROR_HOOKS.includes(hook)) + { + throw new Error(`Error hook ${this.GetConstructorName(hook)} already added.`); + } + + this.ERROR_HOOKS.push(hook); + } + + public static async AddHooksDirectory(directory: string): Promise + { + await FileSystemService.ConfirmDirectoryExistence(directory); + + await this.ParseDirectoryForHooks(directory); + } + + private static async ParseDirectoryForHooks(directory: string): Promise + { + const ENTITIES: Array = await FileSystemService.ReadDirectory(directory); + + for (const ENTITY of ENTITIES) + { + const ENTITY_PATH: string = `${directory}/${ENTITY.name}`; + + if (ENTITY.isDirectory()) + { + await this.ParseDirectoryForHooks(ENTITY_PATH); + + continue; + } + + if (!ENTITY.isFile() && /\.(?:pre|post|error)-hook\./.test(ENTITY.name)) + { + await this.ExtractHook(ENTITY_PATH); + } + } + } + + private static async ExtractHook(path: string): Promise + { + const EXPORTS: unknown = await FileSystemService.Import(path); + + if (isRecord(EXPORTS)) + { + for (const [, EXPORT] of Object.entries(EXPORTS)) + { + if (this.IsPreHook(EXPORT)) + { + this.AddPreHook(EXPORT); + + return; + } + + if (this.IsPostHook(EXPORT)) + { + this.AddPostHook(EXPORT); + + return; + } + + if (this.IsErrorHook(EXPORT)) + { + this.AddErrorHook(EXPORT); + + return; + } + } + } + } + + private static Instantiate(this: void, hook: ConstructorOf | T): T + { + if (isFunction(hook)) + { + return new hook(); + } + + return hook; + } + + private static IsPreHook(value: unknown): value is BasePreHook | ConstructorOf + { + return value instanceof BasePreHook || isFunction(value) && value.prototype instanceof BasePreHook; + } + + private static IsPostHook(value: unknown): value is BasePostHook | ConstructorOf + { + return value instanceof BasePostHook || isFunction(value) && value.prototype instanceof BasePostHook; + } + + private static IsErrorHook(value: unknown): value is BaseErrorHook | ConstructorOf + { + return value instanceof BaseErrorHook || isFunction(value) && value.prototype instanceof BaseErrorHook; + } + + private static GetConstructorName(value: ConstructorOf | object): string + { + if (isFunction(value)) + { + return value.name; + } + + return getConstructorOf(value).name; + } +} + +export { HookRegistry }; diff --git a/packages/architectura/src/core/server/_index.mts b/packages/architectura/src/core/server/_index.mts index 78bc7714..89aa1631 100644 --- a/packages/architectura/src/core/server/_index.mts +++ b/packages/architectura/src/core/server/_index.mts @@ -1,5 +1,5 @@ export * from "./definition/_index.mjs"; -export * from "./global-configuration.mjs"; +export * from "./asset.registry.mjs"; export * from "./rich-client-request.mjs"; export * from "./rich-server-response.mjs"; export * from "./server.mjs"; diff --git a/packages/architectura/src/core/server/asset.registry.mts b/packages/architectura/src/core/server/asset.registry.mts new file mode 100644 index 00000000..946646ab --- /dev/null +++ b/packages/architectura/src/core/server/asset.registry.mts @@ -0,0 +1,24 @@ +import { FileSystemService } from "../../service/file-system/file-system.service.mjs"; + +class AssetRegistry +{ + private static readonly PUBLIC_DIRECTORIES: Map = new Map(); + + /** @internal */ + public static GetPublicDirectories(): ReadonlyMap + { + return this.PUBLIC_DIRECTORIES; + } + + public static async AddPublicDirectory(url_path_start: string, base_directory_path: string): Promise + { + await FileSystemService.ConfirmDirectoryExistence(base_directory_path); + + this.PUBLIC_DIRECTORIES.set( + url_path_start.replace(/\/$/, ""), + base_directory_path.replace(/\/$/, "") + ); + } +} + +export { AssetRegistry }; diff --git a/packages/architectura/src/core/server/global-configuration.mts b/packages/architectura/src/core/server/global-configuration.mts deleted file mode 100644 index c8ee397d..00000000 --- a/packages/architectura/src/core/server/global-configuration.mts +++ /dev/null @@ -1,65 +0,0 @@ -import type { BasePreHook } from "../hook/base.pre-hook.mjs"; -import type { BasePostHook } from "../hook/base.post-hook.mjs"; -import type { BaseErrorHook } from "../hook/base.error-hook.mjs"; -import { FileSystemService } from "../../service/file-system/file-system.service.mjs"; - -class GlobalConfiguration -{ - private static readonly GLOBAL_PRE_HOOKS: Array = []; - private static readonly GLOBAL_POST_HOOKS: Array = []; - private static readonly GLOBAL_ERROR_HOOKS: Array = []; - private static readonly PUBLIC_ASSET_DIRECTORIES: Map = new Map(); - - private constructor() { } - - /** @internal */ - public static GetGlobalPreHooks(): ReadonlyArray - { - return this.GLOBAL_PRE_HOOKS; - } - - public static AddGlobalPreHook(hook: BasePreHook): void - { - this.GLOBAL_PRE_HOOKS.push(hook); - } - - /** @internal */ - public static GetGlobalPostHooks(): ReadonlyArray - { - return this.GLOBAL_POST_HOOKS; - } - - public static AddGlobalPostHook(hook: BasePostHook): void - { - this.GLOBAL_POST_HOOKS.push(hook); - } - - /** @internal */ - public static GetGlobalErrorHooks(): ReadonlyArray - { - return this.GLOBAL_ERROR_HOOKS; - } - - public static AddGlobalErrorHook(hook: BaseErrorHook): void - { - this.GLOBAL_ERROR_HOOKS.push(hook); - } - - /** @internal */ - public static GetPublicAssetDirectories(): ReadonlyMap - { - return this.PUBLIC_ASSET_DIRECTORIES; - } - - public static async AddPublicAssetDirectory(url_path_start: string, base_directory_path: string): Promise - { - await FileSystemService.ConfirmDirectoryExistence(base_directory_path); - - this.PUBLIC_ASSET_DIRECTORIES.set( - url_path_start.replace(/\/$/, ""), - base_directory_path.replace(/\/$/, "") - ); - } -} - -export { GlobalConfiguration }; diff --git a/packages/architectura/src/core/server/server.mts b/packages/architectura/src/core/server/server.mts index 8aa18d3e..6bb802d5 100644 --- a/packages/architectura/src/core/server/server.mts +++ b/packages/architectura/src/core/server/server.mts @@ -12,6 +12,8 @@ import { assertInteger } from "@vitruvius-labs/ts-predicate/type-assertion"; import { FileSystemService } from "../../service/file-system/file-system.service.mjs"; import { LoggerProxy } from "../../service/logger/logger.proxy.mjs"; import { EndpointRegistry } from "../endpoint/endpoint.registry.mjs"; +import { HookRegistry } from "../hook/hook.registry.mjs"; +import { AssetRegistry } from "./asset.registry.mjs"; import { ExecutionContext } from "../execution-context/execution-context.mjs"; import { ExecutionContextRegistry } from "../execution-context/execution-context.registry.mjs"; import { HTTPStatusCodeEnum } from "./definition/enum/http-status-code.enum.mjs"; @@ -19,7 +21,6 @@ import { PortsEnum } from "./definition/enum/ports.enum.mjs"; import { RichClientRequest } from "./rich-client-request.mjs"; import { RichServerResponse } from "./rich-server-response.mjs"; import { ContentType } from "../../utility/content-type/content-type.mjs"; -import { GlobalConfiguration } from "./global-configuration.mjs"; import { HTTPMethodEnum } from "../definition/enum/http-method.enum.mjs"; class Server @@ -109,7 +110,7 @@ class Server return; } - for (const HOOK of GlobalConfiguration.GetGlobalErrorHooks()) + for (const HOOK of HookRegistry.GetErrorHooks()) { await HOOK.execute(CONTEXT, error); } @@ -205,7 +206,7 @@ class Server return undefined; } - for (const [URL_PATH_START, BASE_DIRECTORY_PATH] of GlobalConfiguration.GetPublicAssetDirectories()) + for (const [URL_PATH_START, BASE_DIRECTORY_PATH] of AssetRegistry.GetPublicDirectories()) { if (REQUEST_PATH.startsWith(URL_PATH_START)) { @@ -225,7 +226,7 @@ class Server private static async HandleEndpoints(context: ExecutionContext): Promise { - const ENDPOINT: BaseEndpoint | undefined = this.FindMatchingEndpoint(context.getRequest()); + const ENDPOINT: BaseEndpoint | undefined = EndpointRegistry.FindEndpoint(context.getRequest().getMethod(), context.getRequest().getPath()); if (ENDPOINT === undefined) { @@ -258,39 +259,11 @@ class Server return true; } - private static FindMatchingEndpoint(request: RichClientRequest): BaseEndpoint | undefined - { - const REQUEST_METHOD: string | undefined = request.getMethod(); - const REQUEST_PATH: string = request.getPath(); - const ENDPOINTS: ReadonlyMap = EndpointRegistry.GetEndpoints(); - - for (const [, ENDPOINT] of ENDPOINTS) - { - if (ENDPOINT.getMethod() !== REQUEST_METHOD) - { - continue; - } - - const ROUTE: RegExp = ENDPOINT.getRoute(); - - const MATCHES: RegExpMatchArray | null = REQUEST_PATH.match(ROUTE); - - if (MATCHES !== null) - { - Reflect.set(request, "pathMatchGroups", MATCHES.groups); - - return ENDPOINT; - } - } - - return undefined; - } - private static async RunPreHooks(endpoint: BaseEndpoint, context: ExecutionContext): Promise { const EXCLUDED_HOOKS: Array = endpoint.getExcludedGlobalPreHooks(); - for (const HOOK of GlobalConfiguration.GetGlobalPreHooks()) + for (const HOOK of HookRegistry.GetPreHooks()) { if (EXCLUDED_HOOKS.includes(getConstructorOf(HOOK))) { @@ -310,7 +283,7 @@ class Server { const EXCLUDED_HOOKS: Array = endpoint.getExcludedGlobalPostHooks(); - for (const HOOK of GlobalConfiguration.GetGlobalPostHooks()) + for (const HOOK of HookRegistry.GetPostHooks()) { if (EXCLUDED_HOOKS.includes(getConstructorOf(HOOK))) { @@ -330,7 +303,7 @@ class Server { const EXCLUDED_HOOKS: Array = endpoint.getExcludedGlobalErrorHooks(); - for (const HOOK of GlobalConfiguration.GetGlobalErrorHooks()) + for (const HOOK of HookRegistry.GetErrorHooks()) { if (EXCLUDED_HOOKS.includes(getConstructorOf(HOOK))) { diff --git a/packages/architectura/src/service/logger/logger.service.mts b/packages/architectura/src/service/logger/logger.service.mts index d29f1d4a..357dcbe1 100644 --- a/packages/architectura/src/service/logger/logger.service.mts +++ b/packages/architectura/src/service/logger/logger.service.mts @@ -1,4 +1,4 @@ -import type { LogContextInterface } from "./_index.mjs"; +import type { LogContextInterface } from "./definition/interface/log-context.interface.mjs"; import type { LoggerInterface } from "./definition/interface/logger.interface.mjs"; import { ValidationError } from "@vitruvius-labs/ts-predicate"; import { stringifyErrorTree } from "@vitruvius-labs/ts-predicate/helper"; diff --git a/packages/architectura/src/utility/singleton.mts b/packages/architectura/src/utility/singleton.mts index d2f275b6..4a1854c4 100644 --- a/packages/architectura/src/utility/singleton.mts +++ b/packages/architectura/src/utility/singleton.mts @@ -44,9 +44,46 @@ abstract class Singleton INSTANCES.set(this.constructor, this); } + /** + * Test if there is an instance of a singleton class. + * + * @sealed + * @remarks + * + * This method returns a boolean if the instance of a singleton class exists. + * + * @example + * ```typescript + * class MySingleton extends Singleton + * { + * public constructor() + * { + * super(); + * } + * } + * + * if (!MySingleton.HasInstance(MySingleton)) + * { + * new MySingleton(); + * } + * + * const instance = MySingleton.GetInstance(MySingleton) + * ``` + * + * @param class_constructor - The constructor of the singleton class. + * @returns true if the instance of the singleton class exists, false otherwise. + */ + public static HasInstance(class_constructor: ConstructorOf): boolean + { + const INSTANCE: object | undefined = INSTANCES.get(class_constructor); + + return INSTANCE instanceof class_constructor; + } + /** * Retrieve the instance of a singleton class. * + * @sealed * @remarks * * This method returns the instance of a singleton class. @@ -91,6 +128,7 @@ abstract class Singleton /** * Retrieve the instance of a singleton class. * + * @sealed * @remarks * * This method returns the instance of a singleton class. @@ -114,6 +152,7 @@ abstract class Singleton /** * Clears the instance of a singleton class. * + * @sealed * @remarks * * This method clears the instance of a singleton class. It removes the instance from @@ -131,14 +170,14 @@ abstract class Singleton * * const instance = new MySingleton(); * - * MySingleton.Remove(MySingleton); + * MySingleton.RemoveInstance(MySingleton); * * const instance2 = MySingleton.GetInstance(MySingleton); // Returns undefined * ``` * * @param class_constructor - The constructor of the singleton class. */ - public static Remove(class_constructor: ConstructorOf): void + public static RemoveInstance(class_constructor: ConstructorOf): void { INSTANCES.delete(class_constructor); } diff --git a/packages/architectura/test/core/endpoint/base.endpoint.spec.mts b/packages/architectura/test/core/endpoint/base.endpoint.spec.mts index 6a4fdf69..e2d76dea 100644 --- a/packages/architectura/test/core/endpoint/base.endpoint.spec.mts +++ b/packages/architectura/test/core/endpoint/base.endpoint.spec.mts @@ -1,75 +1,9 @@ -import { deepStrictEqual, strictEqual } from "node:assert"; +import { deepStrictEqual } from "node:assert"; import { describe, it } from "node:test"; import { BaseEndpoint, BaseErrorHook, BasePostHook, BasePreHook, HTTPMethodEnum } from "../../../src/_index.mjs"; describe("BaseEndpoint", (): void => { - describe("getMethod", (): void => - { - it("should return the method", (): void => - { - class DummyEndpoint extends BaseEndpoint - { - protected readonly method: HTTPMethodEnum = HTTPMethodEnum.PUT; - protected readonly route: string = "/test-dummy"; - - public execute(): void { } - } - - const ENDPOINT: DummyEndpoint = new DummyEndpoint(); - - strictEqual(ENDPOINT.getMethod(), HTTPMethodEnum.PUT); - }); - }); - - describe("getRoute", (): void => - { - it("should return the route when it's a RegExp, that RegExp must match the whole path", (): void => - { - class DummyEndpoint extends BaseEndpoint - { - protected readonly method: HTTPMethodEnum = HTTPMethodEnum.GET; - protected readonly route: RegExp = /\/test-dummy/; - - public execute(): void { } - } - - const ENDPOINT: DummyEndpoint = new DummyEndpoint(); - - deepStrictEqual(ENDPOINT.getRoute(), /^\/test-dummy$/); - }); - - it("should return the route as a RegExp when it is a string, that RegExp must match the whole path", (): void => - { - class DummyEndpoint extends BaseEndpoint - { - protected readonly method: HTTPMethodEnum = HTTPMethodEnum.GET; - protected readonly route: string = "/test-dummy"; - - public execute(): void { } - } - - const ENDPOINT: DummyEndpoint = new DummyEndpoint(); - - deepStrictEqual(ENDPOINT.getRoute(), /^\/test-dummy$/); - }); - - it("should preserve named capturing groups", (): void => - { - class DummyEndpoint extends BaseEndpoint - { - protected readonly method: HTTPMethodEnum = HTTPMethodEnum.GET; - protected readonly route: string = "/test-dummy/(?[a-z0-9-]+)"; - - public execute(): void { } - } - - const ENDPOINT: DummyEndpoint = new DummyEndpoint(); - - deepStrictEqual(ENDPOINT.getRoute(), /^\/test-dummy\/(?[a-z0-9-]+)$/); - }); - }); - describe("getPreHooks", (): void => { it("should return the pre hooks", (): void => diff --git a/packages/architectura/test/core/endpoint/endpoint.registry.spec.mts b/packages/architectura/test/core/endpoint/endpoint.registry.spec.mts index 5e8dafd2..e272edaf 100644 --- a/packages/architectura/test/core/endpoint/endpoint.registry.spec.mts +++ b/packages/architectura/test/core/endpoint/endpoint.registry.spec.mts @@ -1,34 +1,26 @@ import { deepStrictEqual } from "node:assert/strict"; import { describe, it } from "node:test"; -import { BaseEndpoint, EndpointRegistry, HTTPMethodEnum, HelloWorldEndpoint, Singleton } from "../../../src/_index.mjs"; +import { BaseEndpoint, type EndpointEntryInterface, EndpointRegistry, HTTPMethodEnum, HelloWorldEndpoint } from "../../../src/_index.mjs"; describe("EndpointRegistry", (): void => { - describe("GetEndpoints", (): void => + describe("FindEndpoint", (): void => { it("should return a map with the HelloWorldEndpoint when no endpoint was registered, but not add it permanently", (): void => { - let endpoint: HelloWorldEndpoint | undefined = Singleton.FindInstance(HelloWorldEndpoint); - - if (endpoint === undefined) - { - endpoint = new HelloWorldEndpoint(); - } + const ENDPOINT: HelloWorldEndpoint = new HelloWorldEndpoint(); const EMPTY_MAP: Map = new Map(); - const HELLO_WORLD_MAP: Map = new Map([ - ["GET::/^.*$/", endpoint], - ]); // @ts-expect-error - We need to access this private property for test purposes. EndpointRegistry.ENDPOINTS.clear(); - deepStrictEqual(EndpointRegistry.GetEndpoints(), HELLO_WORLD_MAP); + deepStrictEqual(EndpointRegistry.FindEndpoint(HTTPMethodEnum.GET, "/"), ENDPOINT); // @ts-expect-error - We need to access this private property for test purposes. deepStrictEqual(EndpointRegistry.ENDPOINTS, EMPTY_MAP); }); - it("should return the registered endpoints", (): void => + it("should return the registered endpoint that matches", (): void => { class DummyEndpoint extends BaseEndpoint { @@ -40,16 +32,61 @@ describe("EndpointRegistry", (): void => const ENDPOINT: DummyEndpoint = new DummyEndpoint(); - const ENDPOINT_MAP: Map = new Map([ - ["dummy-key", ENDPOINT], - ]); + // @ts-expect-error - We need to access this private property for test purposes. + EndpointRegistry.ENDPOINTS.clear(); + + // @ts-expect-error - We need to access this private property for test purposes. + EndpointRegistry.ENDPOINTS.set( + "dummy-key", + { + method: HTTPMethodEnum.GET, + route: /^\/test-dummy$/, + endpoint: ENDPOINT, + } + ); + + deepStrictEqual(EndpointRegistry.FindEndpoint(HTTPMethodEnum.GET, "/test-dummy"), ENDPOINT); + + // @ts-expect-error - We need to access this private property for test purposes. + EndpointRegistry.ENDPOINTS.clear(); + }); + + it("should return undefined if there is no match", (): void => + { + class DummyEndpoint extends BaseEndpoint + { + protected readonly method: HTTPMethodEnum = HTTPMethodEnum.GET; + protected readonly route: string = "/test-dummy"; + + public execute(): void { } + } + + const ENDPOINT: DummyEndpoint = new DummyEndpoint(); // @ts-expect-error - We need to access this private property for test purposes. EndpointRegistry.ENDPOINTS.clear(); + + // @ts-expect-error - We need to access this private property for test purposes. + EndpointRegistry.ENDPOINTS.set( + "dummy-key", + { + method: HTTPMethodEnum.GET, + route: /^\/test-dummy$/, + endpoint: ENDPOINT, + } + ); + // @ts-expect-error - We need to access this private property for test purposes. - EndpointRegistry.ENDPOINTS.set("dummy-key", ENDPOINT); + EndpointRegistry.ENDPOINTS.set( + "dummy-key", + { + method: HTTPMethodEnum.POST, + route: /^\/dummy-test$/, + endpoint: ENDPOINT, + } + ); - deepStrictEqual(EndpointRegistry.GetEndpoints(), ENDPOINT_MAP); + deepStrictEqual(EndpointRegistry.FindEndpoint(HTTPMethodEnum.GET, "/dummy-test"), undefined); // @ts-expect-error - We need to access this private property for test purposes. EndpointRegistry.ENDPOINTS.clear(); @@ -70,8 +107,15 @@ describe("EndpointRegistry", (): void => const ENDPOINT: DummyEndpoint = new DummyEndpoint(); - const POPULATED_MAP: Map = new Map([ - ["GET::/^\\/test-dummy$/", ENDPOINT], + const POPULATED_MAP: Map = new Map([ + [ + "GET::/^\\/test-dummy$/", + { + method: HTTPMethodEnum.GET, + route: /^\/test-dummy$/, + endpoint: ENDPOINT, + }, + ], ]); // @ts-expect-error - We need to access this private property for test purposes. diff --git a/packages/architectura/test/core/execution-context/execution-context.registry.spec.mts b/packages/architectura/test/core/execution-context/execution-context.registry.spec.mts index 301d4bbe..2996065b 100644 --- a/packages/architectura/test/core/execution-context/execution-context.registry.spec.mts +++ b/packages/architectura/test/core/execution-context/execution-context.registry.spec.mts @@ -1,23 +1,31 @@ import { deepStrictEqual, throws } from "node:assert"; import { describe, it } from "node:test"; -import { ExecutionContextRegistry } from "../../../src/_index.mjs"; +import { ExecutionContext, type ExecutionContextInstantiationInterface, ExecutionContextRegistry } from "../../../src/_index.mjs"; describe("ExecutionContextRegistry", (): void => { - describe("GetContextAccessor", (): void => { - it("should return the static ContextAccessor property when called", (): void => - { - // @ts-expect-error - We need to access this private property for test purposes. - deepStrictEqual(ExecutionContextRegistry.GetContextAccessor(), ExecutionContextRegistry.ContextAccessor); - }); - }); - describe("GetExecutionContext", (): void => { it("should throw when called outside of an async hook", (): void => { - throws( - (): void => { - ExecutionContextRegistry.GetExecutionContext(); - } - ); + const WRAPPER = (): void => + { + ExecutionContextRegistry.GetExecutionContext(); + }; + + throws(WRAPPER); + }); + + it("should return a context when called inside of an async hook", (): void => { + const PARAMETERS: ExecutionContextInstantiationInterface = { + // @ts-expect-error: Dummy value for testing purposes. + request: Symbol("request"), + // @ts-expect-error: Dummy value for testing purposes. + response: Symbol("response"), + }; + + const CONTEXT: ExecutionContext = new ExecutionContext(PARAMETERS); + + ExecutionContextRegistry.SetExecutionContext(CONTEXT); + + deepStrictEqual(ExecutionContextRegistry.GetExecutionContext(), CONTEXT); }); }); }); diff --git a/packages/architectura/test/utility/singleton.spec.mts b/packages/architectura/test/utility/singleton.spec.mts index ee324f1f..227d4839 100644 --- a/packages/architectura/test/utility/singleton.spec.mts +++ b/packages/architectura/test/utility/singleton.spec.mts @@ -60,7 +60,7 @@ describe("Singleton", (): void => { }); }); - describe("Remove", (): void => { + describe("RemoveInstance", (): void => { it("should remove the instance of the singleton class.", (): void => { class MySingleton extends Singleton { @@ -72,7 +72,7 @@ describe("Singleton", (): void => { new MySingleton(); - MySingleton.Remove(MySingleton); + MySingleton.RemoveInstance(MySingleton); const INSTANCE2: MySingleton | undefined = MySingleton.FindInstance(MySingleton);