From be1f615858811dae60bc1eb569032dada8bdab5c Mon Sep 17 00:00:00 2001 From: garrettmills Date: Tue, 2 Mar 2021 18:57:41 -0600 Subject: [PATCH] Initial import --- .gitignore | 192 +++++++++++++++++++++++++ .idea/.gitignore | 8 ++ .idea/lib.iml | 8 ++ .idea/modules.xml | 8 ++ package-lock.json | 112 +++++++++++++++ package.json | 33 +++++ src/http/Controller.ts | 3 + src/index.ts | 16 +++ src/lifecycle/AppClass.ts | 56 ++++++++ src/lifecycle/Application.ts | 198 ++++++++++++++++++++++++++ src/lifecycle/RunLevelErrorHandler.ts | 74 ++++++++++ src/lifecycle/Unit.ts | 15 ++ src/service/Canon.ts | 56 ++++++++ src/service/Canonical.ts | 125 ++++++++++++++++ src/service/CanonicalInstantiable.ts | 22 +++ src/service/CanonicalRecursive.ts | 12 ++ src/service/CanonicalStatic.ts | 13 ++ src/service/Config.ts | 9 ++ src/service/Controllers.ts | 20 +++ src/service/FakeCanonical.ts | 7 + src/service/Logging.ts | 81 +++++++++++ src/tsconfig.json | 11 ++ tsconfig.json | 13 ++ 23 files changed, 1092 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/lib.iml create mode 100644 .idea/modules.xml create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/http/Controller.ts create mode 100644 src/index.ts create mode 100644 src/lifecycle/AppClass.ts create mode 100644 src/lifecycle/Application.ts create mode 100644 src/lifecycle/RunLevelErrorHandler.ts create mode 100644 src/lifecycle/Unit.ts create mode 100644 src/service/Canon.ts create mode 100644 src/service/Canonical.ts create mode 100644 src/service/CanonicalInstantiable.ts create mode 100644 src/service/CanonicalRecursive.ts create mode 100644 src/service/CanonicalStatic.ts create mode 100644 src/service/Config.ts create mode 100644 src/service/Controllers.ts create mode 100644 src/service/FakeCanonical.ts create mode 100644 src/service/Logging.ts create mode 100644 src/tsconfig.json create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2840a2c --- /dev/null +++ b/.gitignore @@ -0,0 +1,192 @@ +# ---> JetBrains +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# ---> Node +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +/lib diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..73f69e0 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/lib.iml b/.idea/lib.iml new file mode 100644 index 0000000..c956989 --- /dev/null +++ b/.idea/lib.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..b0c4ff6 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..370f9c8 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,112 @@ +{ + "name": "@extollo/lib", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@extollo/di": { + "version": "file:../di", + "requires": { + "@extollo/util": "file:../util", + "reflect-metadata": "^0.1.13", + "typescript": "^4.1.3" + }, + "dependencies": { + "@extollo/util": { + "version": "file:../util", + "requires": { + "@types/node": "^14.14.20", + "@types/uuid": "^8.3.0", + "colors": "^1.4.0", + "typescript": "^4.1.3", + "uuid": "^8.3.2" + }, + "dependencies": { + "@types/node": { + "version": "14.14.22", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.22.tgz", + "integrity": "sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw==" + }, + "@types/uuid": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz", + "integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==" + }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" + }, + "typescript": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz", + "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + } + } + }, + "reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" + }, + "typescript": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz", + "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==" + } + } + }, + "@extollo/util": { + "version": "file:../util", + "requires": { + "@types/node": "^14.14.20", + "@types/uuid": "^8.3.0", + "colors": "^1.4.0", + "typescript": "^4.1.3", + "uuid": "^8.3.2" + }, + "dependencies": { + "@types/node": { + "version": "14.14.22", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.22.tgz", + "integrity": "sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw==" + }, + "@types/uuid": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz", + "integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==" + }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" + }, + "typescript": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz", + "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + } + } + }, + "dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" + }, + "typescript": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz", + "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..0904b62 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "@extollo/lib", + "version": "0.1.0", + "description": "The framework library that lifts up your code.", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "directories": { + "lib": "lib" + }, + "dependencies": { + "@extollo/di": "file:../di", + "@extollo/util": "file:../util", + "dotenv": "^8.2.0", + "typescript": "^4.1.3" + }, + "devDependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "tsc", + "app": "tsc && node lib/index.js" + }, + "files": [ + "lib/**/*" + ], + "prepare": "npm run build", + "postversion": "git push && git push --tags", + "repository": { + "type": "git", + "url": "https://code.garrettmills.dev/extollo/lib" + }, + "author": "garrettmills ", + "license": "MIT" +} diff --git a/src/http/Controller.ts b/src/http/Controller.ts new file mode 100644 index 0000000..588cb05 --- /dev/null +++ b/src/http/Controller.ts @@ -0,0 +1,3 @@ +import {AppClass} from "../lifecycle/AppClass"; + +export class Controller extends AppClass {} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..0fce67c --- /dev/null +++ b/src/index.ts @@ -0,0 +1,16 @@ +export * from './service/Logging' + +export * from './lifecycle/RunLevelErrorHandler' +export * from './lifecycle/Application' +export * from './lifecycle/AppClass' +export * from './lifecycle/Unit' + +export * from './http/Controller' + +export * from './service/Canonical' +export * from './service/CanonicalInstantiable' +export * from './service/CanonicalRecursive' +export * from './service/CanonicalStatic' +export * from './service/FakeCanonical' +export * from './service/Config' +export * from './service/Controllers' diff --git a/src/lifecycle/AppClass.ts b/src/lifecycle/AppClass.ts new file mode 100644 index 0000000..9afa060 --- /dev/null +++ b/src/lifecycle/AppClass.ts @@ -0,0 +1,56 @@ +import {Application} from './Application'; +import {Container} from "@extollo/di"; + +/** + * Base type for a class that supports binding methods by string. + */ +export interface Bindable { + getBoundMethod(methodName: string): (...args: any[]) => any +} + +/** + * Returns true if the given object is bindable. + * @param what + * @return boolean + */ +export function isBindable(what: any): what is Bindable { + return ( + what + && typeof what.getBoundMethod === 'function' + && what.getBoundMethod.length === 1 + && typeof what.getBoundMethod('getBoundMethod') === 'function' + ) +} + +export class AppClass { + private readonly appClassApplication!: Application; + + constructor() { + this.appClassApplication = Application.getApplication(); + } + + protected app(): Application { + return this.appClassApplication; + } + + protected container(): Container { + return this.appClassApplication; + } + + /** + * Get the method with the given name from this class, bound to this class. + * @param {string} methodName + * @return function + */ + public getBoundMethod(methodName: string): (...args: any[]) => any { + // @ts-ignore + if ( typeof this[methodName] !== 'function' ) { + throw new TypeError(`Attempt to get bound method for non-function type: ${methodName}`) + } + + return (...args: any[]): any => { + // @ts-ignore + return this[methodName](...args) + } + } +} diff --git a/src/lifecycle/Application.ts b/src/lifecycle/Application.ts new file mode 100644 index 0000000..63be160 --- /dev/null +++ b/src/lifecycle/Application.ts @@ -0,0 +1,198 @@ +import {Container} from '@extollo/di'; +import { + ErrorWithContext, + globalRegistry, + infer, + isLoggingLevel, + PathLike, + StandardLogger, + universalPath, + UniversalPath +} from '@extollo/util'; + +import {Logging} from '../service/Logging'; +import {RunLevelErrorHandler} from "./RunLevelErrorHandler"; +import {Unit, UnitStatus} from "./Unit"; +import * as dotenv from 'dotenv'; + +export function env(key: string, defaultValue?: any): any { + return Application.getApplication().env(key, defaultValue) +} + +export class Application extends Container { + public static getContainer(): Container { + const existing = globalRegistry.getGlobal('extollo/injector') + if ( !existing ) { + const container = new Application() + globalRegistry.setGlobal('extollo/injector', container) + return container + } + + return existing as Container + } + + public static getApplication(): Application { + const existing = globalRegistry.getGlobal('extollo/injector') + if ( existing && !(existing instanceof Application) ) { + const app = new Application() + existing.cloneTo(app) + + globalRegistry.setGlobal('extollo/injector', app) + return app + } else if ( !existing ) { + const app = new Application() + globalRegistry.setGlobal('extollo/injector', app) + return app + } + + return existing + } + + protected baseDir!: string + protected basePath!: UniversalPath + protected applicationUnits: (typeof Unit)[] = [] + protected instantiatedUnits: Unit[] = [] + + constructor() { + super() + + if ( !this.hasKey(Application) ) { + this.register(Application) + this.instances.push({ + key: Application, + value: this, + }) + } + + if ( !this.hasKey('app') ) { + this.registerSingleton('app', this) + } + } + + get root() { + return this.basePath.concat() + } + + get appRoot() { + return this.basePath.concat('app') + } + + path(...parts: PathLike[]) { + return this.basePath.concat(...parts) + } + + appPath(...parts: PathLike[]) { + return this.basePath.concat('app', ...parts) + } + + get errorHandler() { + const rleh: RunLevelErrorHandler = this.make(RunLevelErrorHandler) + return rleh.handle + } + + errorWrapContext(e: Error, context: {[key: string]: any}): ErrorWithContext { + const rleh: RunLevelErrorHandler = this.make(RunLevelErrorHandler) + return rleh.wrapContext(e, context) + } + + scaffold(absolutePathToApplicationRoot: string, applicationUnits: (typeof Unit)[]) { + this.baseDir = absolutePathToApplicationRoot + this.basePath = universalPath(absolutePathToApplicationRoot) + this.applicationUnits = applicationUnits + + this.bootstrapEnvironment() + this.setupLogging() + + this.make(Logging).debug(`Application root: ${this.baseDir}`) + } + + protected setupLogging() { + const standard: StandardLogger = this.make(StandardLogger) + const logging: Logging = this.make(Logging) + + logging.registerLogger(standard) + + try { + logging.verbose('Attempting to load logging level from the environment...') + const envLevel = this.env('EXTOLLO_LOGGING_LEVEL') + logging.verbose(`Read logging level: ${envLevel}`) + + if ( isLoggingLevel(envLevel) ) { + logging.verbose('Logging level is valid.') + logging.level = envLevel + logging.debug(`Set logging level from environment: ${envLevel}`) + } + } catch(e) {} + } + + protected bootstrapEnvironment() { + dotenv.config({ + path: this.basePath.concat('.env').toLocal + }) + } + + public env(key: string, defaultValue?: any): any { + return infer(process.env[key] ?? '') ?? defaultValue + } + + async run() { + try { + await this.up() + await this.down() + } catch (e) { + this.errorHandler(e) + } + } + + async up() { + const logging: Logging = this.make(Logging) + + logging.info('Starting Extollo...', true) + for ( const unitClass of this.applicationUnits ) { + const unit: Unit = this.make(unitClass) + this.instantiatedUnits.push(unit) + await this.startUnit(unit) + } + } + + async down() { + const logging: Logging = this.make(Logging) + + logging.info('Stopping Extollo...', true) + for ( const unit of this.instantiatedUnits ) { + if ( !unit ) continue + await this.stopUnit(unit) + } + } + + protected async startUnit(unit: Unit) { + const logging: Logging = this.make(Logging) + + try { + logging.debug(`Starting ${unit.constructor.name}...`) + unit.status = UnitStatus.Starting + await unit.up() + unit.status = UnitStatus.Started + logging.info(`Started ${unit.constructor.name}.`) + } catch (e) { + unit.status = UnitStatus.Error + console.log(e) + throw this.errorWrapContext(e, {unit_name: unit.constructor.name}) + } + } + + protected async stopUnit(unit: Unit) { + const logging: Logging = this.make(Logging) + + try { + logging.debug(`Stopping ${unit.constructor.name}...`) + unit.status = UnitStatus.Stopping + await unit.down() + unit.status = UnitStatus.Stopped + logging.info(`Stopped ${unit.constructor.name}.`) + } catch (e) { + unit.status = UnitStatus.Error + throw this.errorWrapContext(e, {unit_name: unit.constructor.name}) + } + } +} diff --git a/src/lifecycle/RunLevelErrorHandler.ts b/src/lifecycle/RunLevelErrorHandler.ts new file mode 100644 index 0000000..c6e1e99 --- /dev/null +++ b/src/lifecycle/RunLevelErrorHandler.ts @@ -0,0 +1,74 @@ +import * as color from 'colors/safe' +import {Logging} from "../service/Logging"; +import {Inject} from "@extollo/di"; +import {ErrorWithContext} from "@extollo/util"; + +export class RunLevelErrorHandler { + @Inject() + protected logging!: Logging + + /** + * Get the error handler function. + * @type (e: Error) => void + */ + get handle(): (e: Error) => void { + return (e: Error) => { + this.display(e) + process.exit(1) + } + } + + wrapContext(e: Error, context: {[key: string]: any}): ErrorWithContext { + if ( e instanceof ErrorWithContext ) { + e.context = {...e.context, ...context} + return e + } + + const error = new ErrorWithContext(e.message) + error.originalError = e + error.context = context + return error + } + + /** + * Log the error to the logger. + * @param {Error} e + */ + display(e: Error) { + let operativeError = e + let context: {[key: string]: string} = {} + if ( e instanceof ErrorWithContext ) { + if ( e.originalError ) operativeError = e.originalError + context = e.context + } + + const contextDisplay = Object.keys(context).map(key => ` - ${key}: ${context[key]}`).join('\n') + + try { + let errorString = `RunLevelErrorHandler invoked: + +${color.bgRed(' ')} +${color.bgRed(' UNCAUGHT RUN-LEVEL ERROR ')} +${color.bgRed(' ')} + +${e.constructor ? e.constructor.name : e.name} +${color.red(`---------------------------------------------------`)} +${e.stack} +` + + if ( contextDisplay ) { + errorString += ` +With the following context: +${contextDisplay} +` + } + + this.logging.error(errorString, true) + } catch (display_e) { + // The error display encountered an error... + // just throw the original so it makes it out + console.error('RunLevelErrorHandler encountered an error:', display_e.message) + throw operativeError + } + } +} diff --git a/src/lifecycle/Unit.ts b/src/lifecycle/Unit.ts new file mode 100644 index 0000000..f1942e0 --- /dev/null +++ b/src/lifecycle/Unit.ts @@ -0,0 +1,15 @@ +import {AppClass} from './AppClass'; + +export enum UnitStatus { + Starting, + Started, + Stopping, + Stopped, + Error, +} + +export abstract class Unit extends AppClass { + public status: UnitStatus = UnitStatus.Stopped + public up(): Promise | void {} + public down(): Promise | void {} +} diff --git a/src/service/Canon.ts b/src/service/Canon.ts new file mode 100644 index 0000000..dd29208 --- /dev/null +++ b/src/service/Canon.ts @@ -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 } = {} + + /** + * Get a canonical resource by its name key. + * @param {string} key + * @return Canonical + */ + resource(key: string): Canonical { + if ( !this.resources[key] ) throw new NoSuchCanonicalResolverKeyError(key) + return this.resources[key] as Canonical + } + + /** + * Register a canonical resource. + * @param {Canonical} unit + */ + registerCanonical(unit: Canonical) { + const key = unit.canonicalItems + if ( this.resources[key] ) throw new DuplicateResolverKeyError(key) + this.resources[key] = unit + } +} diff --git a/src/service/Canonical.ts b/src/service/Canonical.ts new file mode 100644 index 0000000..ce08b78 --- /dev/null +++ b/src/service/Canonical.ts @@ -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 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 { + return definition.imported.default + } + + protected async buildCanonicalDefinition(filePath: string): Promise { + 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 } + } +} diff --git a/src/service/CanonicalInstantiable.ts b/src/service/CanonicalInstantiable.ts new file mode 100644 index 0000000..53dfe21 --- /dev/null +++ b/src/service/CanonicalInstantiable.ts @@ -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 extends Canonical> { + public async initCanonicalItem(definition: CanonicalDefinition): Promise> { + if ( isInstantiable(definition.imported.default) ) { + return this.app().make(definition.imported.default) + } + + throw new InvalidCanonicalExportError(definition.originalName) + } +} \ No newline at end of file diff --git a/src/service/CanonicalRecursive.ts b/src/service/CanonicalRecursive.ts new file mode 100644 index 0000000..a0d62e7 --- /dev/null +++ b/src/service/CanonicalRecursive.ts @@ -0,0 +1,12 @@ +import {Canonical} from "./Canonical"; + +export class CanonicalRecursive extends Canonical { + 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 + } +} diff --git a/src/service/CanonicalStatic.ts b/src/service/CanonicalStatic.ts new file mode 100644 index 0000000..742b1aa --- /dev/null +++ b/src/service/CanonicalStatic.ts @@ -0,0 +1,13 @@ +import {Canonical, CanonicalDefinition} from "./Canonical"; +import {isStaticClass, StaticClass} from "@extollo/di"; +import {InvalidCanonicalExportError} from "./CanonicalInstantiable"; + +export class CanonicalStatic extends Canonical> { + public async initCanonicalItem(definition: CanonicalDefinition): Promise> { + if ( isStaticClass(definition.imported.default) ) { + return definition.imported.default + } + + throw new InvalidCanonicalExportError(definition.originalName) + } +} diff --git a/src/service/Config.ts b/src/service/Config.ts new file mode 100644 index 0000000..4ad5c03 --- /dev/null +++ b/src/service/Config.ts @@ -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' +} diff --git a/src/service/Controllers.ts b/src/service/Controllers.ts new file mode 100644 index 0000000..4319494 --- /dev/null +++ b/src/service/Controllers.ts @@ -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 { + 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 + } +} diff --git a/src/service/FakeCanonical.ts b/src/service/FakeCanonical.ts new file mode 100644 index 0000000..089b962 --- /dev/null +++ b/src/service/FakeCanonical.ts @@ -0,0 +1,7 @@ +import {Canonical} from "./Canonical"; + +export class FakeCanonical extends Canonical { + public async up() { + this.canon.registerCanonical(this) + } +} diff --git a/src/service/Logging.ts b/src/service/Logging.ts new file mode 100644 index 0000000..f292fb2 --- /dev/null +++ b/src/service/Logging.ts @@ -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] + } +} diff --git a/src/tsconfig.json b/src/tsconfig.json new file mode 100644 index 0000000..eab8150 --- /dev/null +++ b/src/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + "module": "commonjs", + "target": "es5", + "sourceMap": true + }, + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f764f26 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "declaration": true, + "outDir": "./lib", + "strict": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src"], + "exclude": ["node_modules"] +}