Initial import

This commit is contained in:
Garrett Mills 2021-03-02 18:57:41 -06:00
commit be1f615858
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246
23 changed files with 1092 additions and 0 deletions

192
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
import {AppClass} from "../lifecycle/AppClass";
export class Controller extends AppClass {}

16
src/index.ts Normal file
View 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
View 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)
}
}
}

View 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})
}
}
}

View 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
View 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
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]
}
}

11
src/tsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"module": "commonjs",
"target": "es5",
"sourceMap": true
},
"exclude": [
"node_modules"
]
}

13
tsconfig.json Normal file
View 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"]
}