TypeDoc all the thngs

This commit is contained in:
2021-03-25 08:50:13 -05:00
parent 7cb0546b01
commit fad1184afe
52 changed files with 976 additions and 3 deletions

View File

@@ -8,12 +8,18 @@ import {Inject} from "@extollo/di";
import * as nodePath from 'path'
import {Unit} from "../lifecycle/Unit";
/**
* Interface describing a definition of a single canonical item loaded from the app.
*/
export interface CanonicalDefinition {
canonicalName: string,
originalName: string,
imported: any,
}
/**
* Type alias for a function that resolves a canonical name to a canonical item, if one exists.
*/
export type CanonicalResolver<T> = (key: string) => T | undefined
/**
@@ -25,6 +31,19 @@ export interface CanonicalReference {
particular?: string,
}
/**
* Abstract unit type that loads items recursively from a directory structure, assigning
* them normalized names ("canonical names"), and providing a way to fetch the resources
* by name.
*
* @example
* The Config service is a Canonical derivative that loads files ending with `.config.js`
* from the `app/config` directory.
*
* If, for example, there is a config file `app/config/auth/Forms.config.js` (in the
* generated code), it can be loaded by the canonical name `auth:Forms`.
*
*/
export abstract class Canonical<T> extends Unit {
@Inject()
protected readonly logging!: Logging
@@ -81,18 +100,26 @@ export abstract class Canonical<T> extends Unit {
}
}
/**
* Return an array of all loaded canonical names.
*/
public all(): string[] {
return Object.keys(this.loadedItems)
}
/**
* Get a Universal path to the base directory where this unit loads its canonical files from.
*/
public get path(): UniversalPath {
return this.app().appPath(...this.appPath)
}
/** Get the plural name of the canonical items provided by this unit. */
public get canonicalItems() {
return `${this.canonicalItem}s`
}
/** Get a canonical item by key. */
public get(key: string): T | undefined {
if ( key.startsWith('@') ) {
const [namespace, ...rest] = key.split(':')
@@ -112,6 +139,34 @@ export abstract class Canonical<T> extends Unit {
return this.loadedItems[key]
}
/**
* Register a namespace resolver with the canonical unit.
*
* Namespaces are canonical names that start with a particular key, beginning with the `@` character,
* which resolve their resources using a resolver function.
*
* @example
* ```typescript
* const items = {
* 'foo:bar': 123,
* 'bob': 456,
* }
*
* const resolver = (key: string) => items[key]
*
* canonical.registerNamespace('@mynamespace', resolver)
* ```
*
* Now, the items in the `@mynamespace` namespace can be accessed like so:
*
* ```typescript
* canonical.get('@mynamespace:foo:bar') // => 123
* canonical.get('@mynamespace:bob') // => 456
* ```
*
* @param name
* @param resolver
*/
public registerNamespace(name: string, resolver: CanonicalResolver<T>) {
if ( !name.startsWith('@') ) {
throw new ErrorWithContext(`Canonical namespaces must start with @.`, { name })
@@ -139,10 +194,20 @@ export abstract class Canonical<T> extends Unit {
this.canon.registerCanonical(this)
}
/**
* Called for each canonical item loaded from a file. This function should do any setup necessary and return the item
* that should be associated with the canonical name.
* @param definition
*/
public async initCanonicalItem(definition: CanonicalDefinition): Promise<T> {
return definition.imported.default ?? definition.imported[definition.canonicalName.split(':').reverse()[0]]
}
/**
* Given the path to a file in the canonical items directory, create a CanonicalDefinition record from that file.
* @param filePath
* @protected
*/
protected async buildCanonicalDefinition(filePath: string): Promise<CanonicalDefinition> {
const originalName = filePath.replace(this.path.toLocal, '').substr(1)
const pathRegex = new RegExp(nodePath.sep, 'g')

View File

@@ -5,12 +5,18 @@
import {Canonical, CanonicalDefinition} from "./Canonical";
import {Instantiable, isInstantiable} from "@extollo/di";
/**
* Error thrown when the export of a canonical file is determined to be invalid.
*/
export class InvalidCanonicalExportError extends Error {
constructor(name: string) {
super(`Unable to import canonical item from "${name}". The default export of this file is invalid.`)
}
}
/**
* Variant of the Canonical unit whose files export classes which are instantiated using the global container.
*/
export class CanonicalInstantiable<T> extends Canonical<Instantiable<T>> {
public async initCanonicalItem(definition: CanonicalDefinition): Promise<Instantiable<T>> {
if ( isInstantiable(definition.imported.default) ) {
@@ -23,4 +29,4 @@ export class CanonicalInstantiable<T> extends Canonical<Instantiable<T>> {
throw new InvalidCanonicalExportError(definition.originalName)
}
}
}

View File

@@ -1,5 +1,27 @@
import {Canonical} from "./Canonical";
/**
* Variant of the Canonical unit whose accessor allows accessing nested
* properties on the resolved objects.
*
* @example
* The Config unit is a CanonicalRecursive unit. So, once a config file is
* resolved, a particular value in the config file can be retrieved as well:
*
* ```typescript
* // app/config/my/config.config.ts
* {
* foo: {
* bar: 123
* }
* }
* ```
*
* This can be accessed as:
* ```typescript
* config.get('my:config.foo.bar') // => 123
* ```
*/
export class CanonicalRecursive extends Canonical<any> {
public get(key: string, fallback?: any): any | undefined {
const parts = key.split('.')

View File

@@ -2,6 +2,14 @@ import {Canonical, CanonicalDefinition} from "./Canonical";
import {isStaticClass, StaticClass} from "@extollo/di";
import {InvalidCanonicalExportError} from "./CanonicalInstantiable";
/**
* Variant of the Canonical unit whose files export static classes, and these static classes
* are the exports of the class.
*
* @example
* The Controllers class is CanonicalStatic. The various `.controller.ts` files export static
* Controller classes, so the canonical items managed by the Controllers service are `Instantiable<Controller>`.
*/
export class CanonicalStatic<T, T2> extends Canonical<StaticClass<T, T2>> {
public async initCanonicalItem(definition: CanonicalDefinition): Promise<StaticClass<T, T2>> {
if ( isStaticClass(definition.imported.default) ) {

View File

@@ -2,6 +2,9 @@ import {Singleton, Inject} from "@extollo/di";
import {CanonicalRecursive} from "./CanonicalRecursive";
import {Logging} from "./Logging";
/**
* Canonical unit that loads configuration files from `app/configs`.
*/
@Singleton()
export class Config extends CanonicalRecursive {
@Inject()
@@ -10,7 +13,11 @@ export class Config extends CanonicalRecursive {
protected appPath: string[] = ['configs']
protected suffix: string = '.config.js'
protected canonicalItem: string = 'config'
/** If true, all the unique configuration keys will be stored for debugging. */
protected recordConfigAccesses: boolean = false
/** Array of all unique accessed config keys, if `recordConfigAccesses` is true. */
protected accessedKeys: string[] = []
public async up() {

View File

@@ -3,6 +3,9 @@ import {Singleton, Instantiable} from "@extollo/di";
import {Controller} from "../http/Controller";
import {CanonicalDefinition} from "./Canonical";
/**
* A canonical unit that loads the controller classes from `app/http/controllers`.
*/
@Singleton()
export class Controllers extends CanonicalStatic<Instantiable<Controller>, Controller> {
protected appPath = ['http', 'controllers']

View File

@@ -1,5 +1,9 @@
import {Canonical} from "./Canonical";
/**
* Canonical class used for faking canonical units. Here, the canonical resolver
* is registered with the global service, but no files are loaded from the filesystem.
*/
export class FakeCanonical<T> extends Canonical<T> {
public async up() {
this.canon.registerCanonical(this)

View File

@@ -15,6 +15,10 @@ import {error} from "../http/response/ErrorResponseFactory";
import {ExecuteResolvedRoutePreflightHTTPModule} from "../http/kernel/module/ExecuteResolvedRoutePreflightHTTPModule";
import {ExecuteResolvedRoutePostflightHTTPModule} from "../http/kernel/module/ExecuteResolvedRoutePostflightHTTPModule";
/**
* Application unit that starts the HTTP/S server, creates Request and Response objects
* for it, and handles those requests using the HTTPKernel.
*/
@Singleton()
export class HTTPServer extends Unit {
@Inject()
@@ -23,6 +27,7 @@ export class HTTPServer extends Unit {
@Inject()
protected readonly kernel!: HTTPKernel
/** The underlying native Node.js server. */
protected server?: Server
public async up() {

View File

@@ -1,53 +1,120 @@
import {Logger, LoggingLevel, LogMessage} from "@extollo/util";
import {Singleton} from "@extollo/di";
/**
* A singleton service that manages loggers registered in the application, and
* can be used to log output to all of them based on the configured logging level.
*
* This should be used in place of `console.log` as it also supports logging to
* external locations.
*
* @example
* ```typescript
* logging.info('Info level!')
* logging.debug('Some debugging information...')
* logging.warn('A warning!', true) // true, to force it to show, regardless of logging level.
* ```
*/
@Singleton()
export class Logging {
/** Array of Logger implementations that should be logged to. */
protected registeredLoggers: Logger[] = []
/** The currently configured logging level. */
protected currentLevel: LoggingLevel = LoggingLevel.Warning
/** Register a Logger implementation with this service. */
public registerLogger(logger: Logger) {
if ( !this.registeredLoggers.includes(logger) ) {
this.registeredLoggers.push(logger)
}
}
/**
* Remove a Logger implementation from this service, if it is registered.
* @param logger
*/
public unregisterLogger(logger: Logger) {
this.registeredLoggers = this.registeredLoggers.filter(x => x !== logger)
}
/**
* Get the current logging level.
*/
public get level(): LoggingLevel {
return this.currentLevel
}
/**
* Set the current logging level.
* @param level
*/
public set level(level: LoggingLevel) {
this.currentLevel = level
}
/**
* Write a success-level output to the logs.
* @param output
* @param force - if true, output even if outside the current logging level
*/
public success(output: any, force = false) {
this.writeLog(LoggingLevel.Success, output, force)
}
/**
* Write an error-level output to the logs.
* @param output
* @param force - if true, output even if outside the current logging level
*/
public error(output: any, force = false) {
this.writeLog(LoggingLevel.Error, output, force)
}
/**
* Write a warning-level output to the logs.
* @param output
* @param force - if true, output even if outside the current logging level
*/
public warn(output: any, force = false) {
this.writeLog(LoggingLevel.Warning, output, force)
}
/**
* Write an info-level output to the logs.
* @param output
* @param force - if true, output even if outside the current logging level
*/
public info(output: any, force = false) {
this.writeLog(LoggingLevel.Info, output, force)
}
/**
* Write a debugging-level output to the logs.
* @param output
* @param force - if true, output even if outside the current logging level
*/
public debug(output: any, force = false) {
this.writeLog(LoggingLevel.Debug, output, force)
}
/**
* Write a verbose-level output to the logs.
* @param output
* @param force - if true, output even if outside the current logging level
*/
public verbose(output: any, force = false) {
this.writeLog(LoggingLevel.Verbose, output, force)
}
/**
* Helper function to write the given output, at the given logging level, to
* all of the registered loggers.
* @param level
* @param output
* @param force - if true, output even if outside the current logging level
* @protected
*/
protected writeLog(level: LoggingLevel, output: any, force = false) {
const message = this.buildMessage(level, output)
if ( this.currentLevel >= level || force ) {
@@ -61,6 +128,12 @@ export class Logging {
}
}
/**
* Given a level and output item, build a formatted LogMessage with date and caller.
* @param level
* @param output
* @protected
*/
protected buildMessage(level: LoggingLevel, output: any): LogMessage {
return {
level,
@@ -70,6 +143,11 @@ export class Logging {
}
}
/**
* Get the name of the object that called the log method using error traces.
* @param level
* @protected
*/
protected getCallerInfo(level = 5): string {
const e = new Error()
if ( !e.stack ) return 'Unknown'

View File

@@ -3,6 +3,9 @@ import {Singleton, Instantiable} from "@extollo/di";
import {CanonicalDefinition} from "./Canonical";
import {Middleware} from "../http/routing/Middleware";
/**
* A canonical unit that loads the middleware classes from `app/http/middlewares`.
*/
@Singleton()
export class Middlewares extends CanonicalStatic<Instantiable<Middleware>, Middleware> {
protected appPath = ['http', 'middlewares']

View File

@@ -6,6 +6,9 @@ import {Route} from "../http/routing/Route";
import {HTTPMethod} from "../http/lifecycle/Request";
import {ViewEngineFactory} from "../views/ViewEngineFactory";
/**
* Application unit that loads the various route files from `app/http/routes` and pre-compiles the route handlers.
*/
@Singleton()
export class Routing extends Unit {
@Inject()
@@ -35,12 +38,21 @@ export class Routing extends Unit {
})
}
/**
* Given an HTTPMethod and route path, return the Route instance that matches them,
* if one exists.
* @param method
* @param path
*/
public match(method: HTTPMethod, path: string): Route | undefined {
return this.compiledRoutes.firstWhere(route => {
return route.match(method, path)
})
}
/**
* Get the universal path to the root directory of the route definitions.
*/
public get path(): UniversalPath {
return this.app().appPath('http', 'routes')
}