Initial import

This commit is contained in:
2021-03-02 18:57:41 -06:00
commit be1f615858
23 changed files with 1092 additions and 0 deletions

56
src/service/Canon.ts Normal file
View File

@@ -0,0 +1,56 @@
import {Canonical} from "./Canonical";
import {Singleton} from "@extollo/di";
/**
* Error throw when a duplicate canonical key is registered.
* @extends Error
*/
export class DuplicateResolverKeyError extends Error {
constructor(key: string) {
super(`There is already a canonical unit with the scope ${key} registered.`)
}
}
/**
* Error throw when a key that isn't registered with the service.
* @extends Error
*/
export class NoSuchCanonicalResolverKeyError extends Error {
constructor(key: string) {
super(`There is no such canonical unit with the scope ${key} registered.`)
}
}
/**
* Service used to access various canonical resources.
*/
@Singleton()
export class Canon {
/**
* The resources registered with this service. Map of canonical service name
* to canonical service instance.
* @type object
*/
protected resources: { [key: string]: Canonical<any> } = {}
/**
* Get a canonical resource by its name key.
* @param {string} key
* @return Canonical
*/
resource<T>(key: string): Canonical<T> {
if ( !this.resources[key] ) throw new NoSuchCanonicalResolverKeyError(key)
return this.resources[key] as Canonical<T>
}
/**
* Register a canonical resource.
* @param {Canonical} unit
*/
registerCanonical(unit: Canonical<any>) {
const key = unit.canonicalItems
if ( this.resources[key] ) throw new DuplicateResolverKeyError(key)
this.resources[key] = unit
}
}

125
src/service/Canonical.ts Normal file
View File

@@ -0,0 +1,125 @@
/**
* Base type for a canonical definition.
*/
import {Canon} from "./Canon";
import {universalPath, UniversalPath} from "@extollo/util";
import {Logging} from "./Logging";
import {Inject} from "@extollo/di";
import * as nodePath from 'path'
import {Unit} from "../lifecycle/Unit";
export interface CanonicalDefinition {
canonicalName: string,
originalName: string,
imported: any,
}
/**
* Base type for a canonical name reference.
*/
export interface CanonicalReference {
resource?: string,
item: string,
particular?: string,
}
export abstract class Canonical<T> extends Unit {
@Inject()
protected readonly logging!: Logging
@Inject()
protected readonly canon!: Canon
/**
* The base path directory where the canonical definitions reside.
* @type string
*/
protected appPath: string[] = []
/**
* The file suffix of files in the base path that should be loaded.
* @type string
*/
protected suffix: string = '.js'
/**
* The singular, programmatic name of one of these canonical items.
* @example middleware
* @type string
*/
protected canonicalItem: string = ''
/**
* Object mapping canonical names to loaded file references.
* @type object
*/
protected loadedItems: { [key: string]: T } = {}
/**
* Resolve a canonical reference from its string form to a CanonicalReference.
* @param {string} reference
* @return CanonicalReference
*/
public static resolve(reference: string): CanonicalReference {
const rscParts = reference.split('::')
const resource = rscParts.length > 1 ? rscParts[0] + 's' : undefined
const rsc_less = rscParts.length > 1 ? rscParts[1] : rscParts[0]
const prtParts = rsc_less.split('.')
const item = prtParts[0]
const particular = prtParts.length > 1 ? prtParts.slice(1).join('.') : undefined
return {
resource, item, particular
}
}
public all(): string[] {
return Object.keys(this.loadedItems)
}
public get path(): UniversalPath {
return this.app().appPath(...this.appPath)
}
public get canonicalItems() {
return `${this.canonicalItem}s`
}
public get(key: string): T | undefined {
return this.loadedItems[key]
}
public async up() {
for await ( const entry of this.path.walk() ) {
if ( !entry.endsWith(this.suffix) ) {
this.logging.debug(`Skipping file with invalid suffix: ${entry}`)
continue
}
const definition = await this.buildCanonicalDefinition(entry)
this.logging.verbose(`Registering canonical ${this.canonicalItem} "${definition.canonicalName}" from ${entry}`)
this.loadedItems[definition.canonicalName] = await this.initCanonicalItem(definition)
}
this.canon.registerCanonical(this)
}
public async initCanonicalItem(definition: CanonicalDefinition): Promise<T> {
return definition.imported.default
}
protected async buildCanonicalDefinition(filePath: string): Promise<CanonicalDefinition> {
const originalName = filePath.replace(this.path.toLocal, '').substr(1)
const pathRegex = new RegExp(nodePath.sep, 'g')
const canonicalName = originalName.replace(pathRegex, ':')
.split('').reverse().join('')
.substr(this.suffix.length)
.split('').reverse().join('')
const fullUniversalPath = universalPath(filePath)
this.logging.verbose(`Importing from: ${fullUniversalPath}`)
const imported = await import(fullUniversalPath.toLocal)
return { canonicalName, originalName, imported }
}
}

View File

@@ -0,0 +1,22 @@
/**
* Error thrown when the item returned from a canonical definition file is not the expected item.
* @extends Error
*/
import {Canonical, CanonicalDefinition} from "./Canonical";
import {Instantiable, isInstantiable} from "@extollo/di";
export class InvalidCanonicalExportError extends Error {
constructor(name: string) {
super(`Unable to import canonical item from "${name}". The default export of this file is invalid.`)
}
}
export class CanonicalInstantiable<T> extends Canonical<Instantiable<T>> {
public async initCanonicalItem(definition: CanonicalDefinition): Promise<Instantiable<T>> {
if ( isInstantiable(definition.imported.default) ) {
return this.app().make(definition.imported.default)
}
throw new InvalidCanonicalExportError(definition.originalName)
}
}

View File

@@ -0,0 +1,12 @@
import {Canonical} from "./Canonical";
export class CanonicalRecursive extends Canonical<any> {
public get(key: string, fallback?: any): any | undefined {
const parts = key.split('.')
let currentValue = this.loadedItems
for ( const part of parts ) {
currentValue = currentValue?.[part]
}
return currentValue ?? fallback
}
}

View File

@@ -0,0 +1,13 @@
import {Canonical, CanonicalDefinition} from "./Canonical";
import {isStaticClass, StaticClass} from "@extollo/di";
import {InvalidCanonicalExportError} from "./CanonicalInstantiable";
export class CanonicalStatic<T, T2> extends Canonical<StaticClass<T, T2>> {
public async initCanonicalItem(definition: CanonicalDefinition): Promise<StaticClass<T, T2>> {
if ( isStaticClass(definition.imported.default) ) {
return definition.imported.default
}
throw new InvalidCanonicalExportError(definition.originalName)
}
}

9
src/service/Config.ts Normal file
View File

@@ -0,0 +1,9 @@
import {Singleton} from "@extollo/di";
import {CanonicalRecursive} from "./CanonicalRecursive";
@Singleton()
export class Config extends CanonicalRecursive {
protected appPath: string[] = ['configs']
protected suffix: string = '.config.js'
protected canonicalItem: string = 'config'
}

View File

@@ -0,0 +1,20 @@
import {CanonicalInstantiable} from "./CanonicalInstantiable";
import {Singleton} from "@extollo/di";
import {Controller} from "../http/Controller";
import {CanonicalDefinition} from "./Canonical";
@Singleton()
export class Controllers extends CanonicalInstantiable<Controller> {
protected appPath = ['http', 'controllers']
protected canonicalItem = 'controller'
protected suffix = '.controller.js'
public async initCanonicalItem(definition: CanonicalDefinition) {
const item = await super.initCanonicalItem(definition)
if ( !(item instanceof Controller) ) {
throw new TypeError(`Invalid controller definition: ${definition.originalName}. Controllers must extend from @extollo/lib.http.Controller.`)
}
return item
}
}

View File

@@ -0,0 +1,7 @@
import {Canonical} from "./Canonical";
export class FakeCanonical<T> extends Canonical<T> {
public async up() {
this.canon.registerCanonical(this)
}
}

81
src/service/Logging.ts Normal file
View File

@@ -0,0 +1,81 @@
import {Logger, LoggingLevel, LogMessage} from "@extollo/util";
import {Singleton} from "@extollo/di";
@Singleton()
export class Logging {
protected registeredLoggers: Logger[] = []
protected currentLevel: LoggingLevel = LoggingLevel.Warning
public registerLogger(logger: Logger) {
if ( !this.registeredLoggers.includes(logger) ) {
this.registeredLoggers.push(logger)
}
}
public unregisterLogger(logger: Logger) {
this.registeredLoggers = this.registeredLoggers.filter(x => x !== logger)
}
public get level(): LoggingLevel {
return this.currentLevel
}
public set level(level: LoggingLevel) {
this.currentLevel = level
}
public success(output: any, force = false) {
this.writeLog(LoggingLevel.Success, output, force)
}
public error(output: any, force = false) {
this.writeLog(LoggingLevel.Error, output, force)
}
public warn(output: any, force = false) {
this.writeLog(LoggingLevel.Warning, output, force)
}
public info(output: any, force = false) {
this.writeLog(LoggingLevel.Info, output, force)
}
public debug(output: any, force = false) {
this.writeLog(LoggingLevel.Debug, output, force)
}
public verbose(output: any, force = false) {
this.writeLog(LoggingLevel.Verbose, output, force)
}
protected writeLog(level: LoggingLevel, output: any, force = false) {
const message = this.buildMessage(level, output)
if ( this.currentLevel >= level || force ) {
for ( const logger of this.registeredLoggers ) {
try {
logger.write(message)
} catch (e) {
console.error('logging error', e)
}
}
}
}
protected buildMessage(level: LoggingLevel, output: any): LogMessage {
return {
level,
output,
date: new Date,
callerName: this.getCallerInfo(),
}
}
protected getCallerInfo(level = 5): string {
const e = new Error()
if ( !e.stack ) return 'Unknown'
return e.stack.split(/\s+at\s+/)
.slice(level)
.map((x: string): string => x.trim().split(' (')[0].split('.')[0].split(':')[0])[0]
}
}