Initial import
This commit is contained in:
56
src/service/Canon.ts
Normal file
56
src/service/Canon.ts
Normal 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
125
src/service/Canonical.ts
Normal 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 }
|
||||
}
|
||||
}
|
||||
22
src/service/CanonicalInstantiable.ts
Normal file
22
src/service/CanonicalInstantiable.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
12
src/service/CanonicalRecursive.ts
Normal file
12
src/service/CanonicalRecursive.ts
Normal 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
|
||||
}
|
||||
}
|
||||
13
src/service/CanonicalStatic.ts
Normal file
13
src/service/CanonicalStatic.ts
Normal 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
9
src/service/Config.ts
Normal 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'
|
||||
}
|
||||
20
src/service/Controllers.ts
Normal file
20
src/service/Controllers.ts
Normal 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
|
||||
}
|
||||
}
|
||||
7
src/service/FakeCanonical.ts
Normal file
7
src/service/FakeCanonical.ts
Normal 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
81
src/service/Logging.ts
Normal 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]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user