Initial import
This commit is contained in:
commit
be1f615858
192
.gitignore
vendored
Normal file
192
.gitignore
vendored
Normal file
@ -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
|
8
.idea/.gitignore
vendored
Normal file
8
.idea/.gitignore
vendored
Normal file
@ -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/
|
8
.idea/lib.iml
Normal file
8
.idea/lib.iml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/lib.iml" filepath="$PROJECT_DIR$/.idea/lib.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
112
package-lock.json
generated
Normal file
112
package-lock.json
generated
Normal file
@ -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=="
|
||||
}
|
||||
}
|
||||
}
|
33
package.json
Normal file
33
package.json
Normal file
@ -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 <shout@garrettmills.dev>",
|
||||
"license": "MIT"
|
||||
}
|
3
src/http/Controller.ts
Normal file
3
src/http/Controller.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import {AppClass} from "../lifecycle/AppClass";
|
||||
|
||||
export class Controller extends AppClass {}
|
16
src/index.ts
Normal file
16
src/index.ts
Normal file
@ -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'
|
56
src/lifecycle/AppClass.ts
Normal file
56
src/lifecycle/AppClass.ts
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
198
src/lifecycle/Application.ts
Normal file
198
src/lifecycle/Application.ts
Normal file
@ -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 = <Container | undefined> 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 = <Container | undefined> 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>(RunLevelErrorHandler)
|
||||
return rleh.handle
|
||||
}
|
||||
|
||||
errorWrapContext(e: Error, context: {[key: string]: any}): ErrorWithContext {
|
||||
const rleh: RunLevelErrorHandler = this.make<RunLevelErrorHandler>(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>(Logging).debug(`Application root: ${this.baseDir}`)
|
||||
}
|
||||
|
||||
protected setupLogging() {
|
||||
const standard: StandardLogger = this.make<StandardLogger>(StandardLogger)
|
||||
const logging: Logging = this.make<Logging>(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)
|
||||
|
||||
logging.info('Starting Extollo...', true)
|
||||
for ( const unitClass of this.applicationUnits ) {
|
||||
const unit: Unit = this.make<Unit>(unitClass)
|
||||
this.instantiatedUnits.push(unit)
|
||||
await this.startUnit(unit)
|
||||
}
|
||||
}
|
||||
|
||||
async down() {
|
||||
const logging: Logging = this.make<Logging>(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>(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>(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})
|
||||
}
|
||||
}
|
||||
}
|
74
src/lifecycle/RunLevelErrorHandler.ts
Normal file
74
src/lifecycle/RunLevelErrorHandler.ts
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
15
src/lifecycle/Unit.ts
Normal file
15
src/lifecycle/Unit.ts
Normal file
@ -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> | void {}
|
||||
public down(): Promise<void> | void {}
|
||||
}
|
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]
|
||||
}
|
||||
}
|
11
src/tsconfig.json
Normal file
11
src/tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"sourceMap": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
13
tsconfig.json
Normal file
13
tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"outDir": "./lib",
|
||||
"strict": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
Loading…
Reference in New Issue
Block a user