Http Kernel!

This commit is contained in:
garrettmills 2020-07-22 09:38:17 -05:00
parent 60bb9afa29
commit b9f2f844f3
No known key found for this signature in database
GPG Key ID: 6ACD58D6ADACFC6E
25 changed files with 402 additions and 43 deletions

View File

@ -2,3 +2,5 @@ export { default as ConfigUnit } from '../../lib/src/unit/Config.ts'
export { DatabaseUnit } from '../../orm/src/DatabaseUnit.ts'
export { default as ControllerUnit } from '../../lib/src/unit/Controllers.ts'
export { default as MiddlewareUnit } from '../../lib/src/unit/Middlewares.ts'
export { default as RoutesUnit } from '../../lib/src/unit/Routes.ts'
export { default as HttpKernelUnit } from '../../lib/src/unit/HttpKernel.ts'

View File

@ -1,4 +1,9 @@
export default {
port: 8080,
use_ssl: false,
powered_by: {
enable: true,
text: 'Daton',
},
}

View File

@ -0,0 +1,5 @@
import Middleware from '../../../lib/src/http/Middleware.ts'
export default class TestMiddleware extends Middleware {
}

View File

@ -1,8 +1,17 @@
import {ConfigUnit, DatabaseUnit, ControllerUnit, MiddlewareUnit} from './bundle/daton_units.ts'
import {
ConfigUnit,
DatabaseUnit,
ControllerUnit,
MiddlewareUnit,
RoutesUnit,
HttpKernelUnit
} from './bundle/daton_units.ts'
export default [
ConfigUnit,
DatabaseUnit,
HttpKernelUnit,
MiddlewareUnit,
ControllerUnit,
RoutesUnit,
]

View File

@ -205,6 +205,17 @@ class Collection<T> {
return new Collection(this._items.filter(func))
}
find<T2>(func: KeyFunction<T, T2>): number | undefined {
let found_index: number | undefined = undefined
this._items.some((item, index) => {
if ( func(item, index) ) {
found_index = index
return true
}
})
return found_index
}
when<T2>(bool: boolean, then: CollectionFunction<T, T2>): Collection<T> {
if ( bool ) then(this)
return this

View File

@ -2,3 +2,4 @@ export * from 'https://deno.land/std@0.53.0/fmt/colors.ts'
export { config as dotenv } from 'https://deno.land/x/dotenv/mod.ts'
export * as path from 'https://deno.land/std@0.53.0/path/mod.ts'
export * as fs from 'https://deno.land/std@0.53.0/fs/mod.ts'
export { generate as uuid } from 'https://deno.land/std/uuid/v4.ts'

View File

@ -1,4 +1,5 @@
import AppClass from '../lifecycle/AppClass.ts'
export default class Controller {
export default class Controller extends AppClass {
}

View File

@ -2,6 +2,7 @@ import { Injectable } from '../../../di/src/decorator/Injection.ts'
import { getCookies, setCookie, delCookie, ServerRequest } from '../external/http.ts'
import { InMemCache } from '../support/InMemCache.ts'
import { HTTPRequest } from './type/HTTPRequest.ts'
import {logger} from "../service/logging/global.ts";
export interface Cookie {
key: string,
@ -23,10 +24,15 @@ export class CookieJar {
this._parsed = getCookies(this.request.to_native)
}
public async get_raw(key: string): Promise<string | undefined> {
return this._parsed[key]
}
public async get(key: string): Promise<MaybeCookie> {
// Try the cache
if ( await this._cache.has(key) )
return this._cache.fetch(key)
if ( await this._cache.has(key) ) {
return JSON.parse((await this._cache.fetch(key)) || '""') as Cookie
}
// If the cache missed, try to parse it and load in cache
if ( key in this._parsed ) {
@ -41,7 +47,7 @@ export class CookieJar {
original_value: this._parsed[key],
}
await this._cache.put(key, cookie)
await this._cache.put(key, JSON.stringify(cookie))
return cookie
}
}
@ -54,7 +60,7 @@ export class CookieJar {
original_value,
}
await this._cache.put(key, value)
await this._cache.put(key, JSON.stringify(cookie))
setCookie(this.request.response, { name: key, value: original_value })
}

View File

@ -1,3 +1,5 @@
export class Middleware {
import AppClass from '../lifecycle/AppClass.ts'
export default class Middleware extends AppClass {
}

View File

@ -1,7 +1,7 @@
import {HTTPResponse} from './type/HTTPResponse.ts'
import {HTTPRequest} from './type/HTTPRequest.ts'
import {ServerRequest} from '../external/http.ts'
import {CookieJar} from "./CookieJar.ts";
import {CookieJar} from './CookieJar.ts'
export class Response implements HTTPResponse {
public status = 200

View File

@ -0,0 +1,93 @@
import Module from './Module.ts'
import Instantiable from '../../../../di/src/type/Instantiable.ts'
import AppClass from '../../lifecycle/AppClass.ts'
import {Collection} from '../../collection/Collection.ts'
import {Service} from '../../../../di/src/decorator/Service.ts'
import {Request} from '../Request.ts'
export interface ModuleRegistrationFluency {
before: (other?: Instantiable<Module>) => Kernel,
after: (other?: Instantiable<Module>) => Kernel,
first: () => Kernel,
last: () => Kernel,
}
export class KernelModuleNotFoundError extends Error {
constructor(mod_name: string) {
super(`The kernel module ${mod_name} is not registered with the kernel.`)
}
}
@Service()
export default class Kernel extends AppClass {
protected preflight: Collection<Module> = new Collection<Module>()
protected postflight: Collection<Module> = new Collection<Module>()
public async handle(request: Request): Promise<Request> {
for ( const module of this.preflight.toArray() ) {
request = await module.apply(request)
}
for ( const module of this.postflight.toArray() ) {
request = await module.apply(request)
}
return request
}
public register(module: Instantiable<Module>): ModuleRegistrationFluency {
return {
before: (other?: Instantiable<Module>): Kernel => {
if ( !other ) {
this.preflight = this.preflight.push(this.make(module))
return this
}
let found_index = this.preflight.find((mod: Module) => mod instanceof other)
if ( typeof found_index !== 'undefined' ) {
this.preflight = this.preflight.put(found_index, this.make(module))
} else {
found_index = this.postflight.find((mod: Module) => mod instanceof other)
}
if ( typeof found_index !== 'undefined' ) {
this.postflight = this.postflight.put(found_index, this.make(module))
} else {
throw new KernelModuleNotFoundError(other.name)
}
return this
},
after: (other?: Instantiable<Module>): Kernel => {
if ( !other ) {
this.postflight = this.postflight.push(this.make(module))
return this
}
let found_index = this.preflight.find((mod: Module) => mod instanceof other)
if ( typeof found_index !== 'undefined' ) {
this.preflight = this.preflight.put(found_index + 1, this.make(module))
} else {
found_index = this.postflight.find((mod: Module) => mod instanceof other)
}
if ( typeof found_index !== 'undefined' ) {
this.postflight = this.postflight.put(found_index + 1, this.make(module))
} else {
console.log(this.preflight, this.postflight)
throw new KernelModuleNotFoundError(other.name)
}
return this
},
first: (): Kernel => {
this.preflight = this.preflight.put(0, this.make(module))
return this
},
last: (): Kernel => {
this.postflight = this.postflight.push(this.make(module))
return this
},
}
}
}

View File

@ -0,0 +1,16 @@
import {Request} from '../Request.ts'
import Kernel from './Kernel.ts'
export default class Module {
public async match(request: Request): Promise<boolean> {
return true
}
public async apply(request: Request): Promise<Request> {
return request
}
public static register(kernel: Kernel) {
kernel.register(this).before()
}
}

View File

@ -0,0 +1,16 @@
import Module from '../Module.ts'
import Kernel from '../Kernel.ts'
import {Request} from '../../Request.ts'
export default class PrepareRequest extends Module {
public static register(kernel: Kernel) {
kernel.register(this).first()
}
public async apply(request: Request): Promise<Request> {
await request.prepare()
return request
}
}

View File

@ -0,0 +1,24 @@
import Module from '../Module.ts'
import Kernel from '../Kernel.ts'
import {Request} from '../../Request.ts'
import {Injectable} from '../../../../../di/src/decorator/Injection.ts'
import Config from '../../../unit/Config.ts'
@Injectable()
export default class SetDatonHeaders extends Module {
public static register(kernel: Kernel) {
kernel.register(this).after()
}
constructor(
protected readonly config: Config,
) {
super()
}
public async apply(request: Request): Promise<Request> {
const text = this.config.get('server.powered_by.text', 'Daton')
request.response.headers.set('X-Powered-By', text)
return request
}
}

View File

@ -0,0 +1,28 @@
import Module from '../Module.ts'
import Kernel from '../Kernel.ts'
import {Request} from '../../Request.ts'
import PrepareRequest from './PrepareRequest.ts'
import Utility from '../../../service/utility/Utility.ts'
import {Injectable} from '../../../../../di/src/decorator/Injection.ts'
@Injectable()
export default class SetSessionCookie extends Module {
public static register(kernel: Kernel) {
kernel.register(this).after(PrepareRequest)
}
constructor(
protected utility: Utility,
) {
super()
}
public async apply(request: Request): Promise<Request> {
if ( !(await request.cookies.has('daton.session')) ) {
await request.cookies.set('daton.session', `${this.utility.uuid()}-${this.utility.uuid()}`)
}
return request
}
}

View File

@ -0,0 +1,77 @@
import {logger} from '../../service/logging/global.ts'
export type RouteVerb = 'get' | 'post' | 'patch' | 'delete' | 'head' | 'put' | 'connect' | 'options' | 'trace'
export type RouteVerbGroup = { [key: string]: string | string[] }
export interface RouterDefinition {
prefix?: string,
middleware?: string[],
get?: RouteVerbGroup,
post?: RouteVerbGroup,
patch?: RouteVerbGroup,
delete?: RouteVerbGroup,
head?: RouteVerbGroup,
put?: RouteVerbGroup,
connect?: RouteVerbGroup,
options?: RouteVerbGroup,
trace?: RouteVerbGroup,
}
export function isRouteVerb(something: any): something is RouteVerb {
const route_verbs = ['get', 'post', 'patch', 'delete', 'head', 'put', 'connect', 'options', 'trace']
return route_verbs.includes(something)
}
export function isRouteVerbGroup(something: any): something is RouteVerbGroup {
if ( !(typeof something === 'object' ) ) return false
for ( const key in something ) {
if ( !something.hasOwnProperty(key) ) continue
if ( typeof key !== 'string' ) {
logger.debug(`Route verb group key is not a string: ${key}`)
return false
}
if (
!(typeof something[key] === 'string')
&& !(Array.isArray(something[key]) && something[key].every((x: any) => typeof x === 'string'))
) {
logger.info(`Route verb group for key ${key} is not a string or array of strings.`)
return false
}
}
return true
}
export function isRouterDefinition(something: any): something is RouterDefinition {
if ( !(typeof something === 'object') ) {
logger.debug('Routing definition is not an object.')
return false
}
for ( const key in something ) {
if (!something.hasOwnProperty(key)) continue
if ( key === 'prefix' ) {
if ( typeof something[key] !== 'string' ) {
logger.debug(`Invalid route prefix: ${something[key]}`)
return false
}
}
else if ( key === 'middleware' ) {
if ( !Array.isArray(something[key]) ) {
logger.debug('Middleware is not an array.')
return false
}
else if ( !something[key].every((x: any) => typeof x === 'string') ) {
logger.debug('Middleware array contains non-string values.')
return false
}
} else if ( isRouteVerb(key) ) {
if ( !isRouteVerbGroup(something[key as any]) ) {
logger.debug('Verb group value is not a valid route verb group.')
return false
}
} else {
logger.debug(`Invalid key: ${key}`)
return false
}
}
return true
}

View File

@ -0,0 +1,22 @@
import Instantiable from '../../../di/src/type/Instantiable.ts'
import {DependencyKey} from '../../../di/src/type/DependencyKey.ts'
import {make} from '../../../di/src/global.ts'
import Application from '../lifecycle/Application.ts'
export default class AppClass {
protected static make<T>(target: Instantiable<T>|DependencyKey, ...parameters: any[]) {
return make(target, ...parameters)
}
protected static get app() {
return make(Application)
}
protected make<T>(target: Instantiable<T>|DependencyKey, ...parameters: any[]) {
return make(target, ...parameters)
}
protected get app() {
return make(Application)
}
}

View File

@ -3,12 +3,13 @@ import { Collection } from '../collection/Collection.ts'
import {container, make} from '../../../di/src/global.ts'
import {DependencyKey} from '../../../di/src/type/DependencyKey.ts'
import Instantiable, {isInstantiable} from '../../../di/src/type/Instantiable.ts'
import AppClass from './AppClass.ts'
const isLifecycleUnit = (something: any): something is (typeof LifecycleUnit) => {
return isInstantiable(something) && something.prototype instanceof LifecycleUnit
}
export default abstract class LifecycleUnit {
export default abstract class LifecycleUnit extends AppClass {
private _status = Status.Stopped
public get status() {
@ -34,8 +35,4 @@ export default abstract class LifecycleUnit {
}
return new Collection<typeof LifecycleUnit>()
}
protected make<T>(target: Instantiable<T>|DependencyKey, ...parameters: any[]) {
return make(target, ...parameters)
}
}

View File

@ -1,5 +1,6 @@
import { Service } from '../../../../di/src/decorator/Service.ts'
import { Logging } from '../logging/Logging.ts'
import {uuid} from '../../external/std.ts'
@Service()
export default class Utility {
@ -52,4 +53,8 @@ export default class Utility {
return false
}
}
uuid(): string {
return uuid()
}
}

View File

@ -1,35 +1,31 @@
import {Service} from '../../../di/src/decorator/Service.ts'
export type CanonicalResolver = (key: string) => any
import {Canonical} from './Canonical.ts'
export class DuplicateResolverKeyError extends Error {
constructor(key: string) {
super(`There is already a canonical resource with the scope ${key} registered.`)
super(`There is already a canonical unit with the scope ${key} registered.`)
}
}
export class NoSuchCanonicalResolverKeyError extends Error {
constructor(key: string) {
super(`There is no such canonical unit with the scope ${key} registered.`)
}
}
@Service()
export class Canon {
protected resources: { [key: string]: any } = {}
protected resources: { [key: string]: Canonical<any> } = {}
get(key: string): any {
const key_parts = key.split('::')
let desc_value = this.resources
key_parts.forEach(part => {
if ( typeof desc_value === 'function' ) {
desc_value = desc_value(part)
} else {
desc_value = desc_value[part]
}
})
return desc_value
resource<T>(key: string): Canonical<T> {
if ( !this.resources[key] ) throw new NoSuchCanonicalResolverKeyError(key)
return this.resources[key] as Canonical<T>
}
register_resource(scope: string, resolver: CanonicalResolver) {
if ( this.resources[scope] ) {
throw new DuplicateResolverKeyError(scope)
}
this.resources[scope] = resolver
register_canonical(unit: Canonical<any>) {
const key = unit.canonical_items
if ( this.resources[key] ) throw new DuplicateResolverKeyError(key)
this.resources[key] = unit
}
}

View File

@ -28,8 +28,7 @@ export class Canonical<T> extends LifecycleUnit {
const def = await this._get_canonical_definition(entry.path)
this._items[def.canonical_name] = await this.init_canonical_item(def)
}
this.make(Canon).register_resource(this.canonical_items, (key: string) => this.get(key))
this.make(Canon).register_canonical(this)
}
public async init_canonical_item(definition: CanonicalDefinition): Promise<T> {

View File

@ -0,0 +1,28 @@
import LifecycleUnit from "../lifecycle/Unit.ts";
import {Unit} from "../lifecycle/decorators.ts";
import Kernel from "../http/kernel/Kernel.ts";
import PrepareRequest from "../http/kernel/module/PrepareRequest.ts";
import SetSessionCookie from "../http/kernel/module/SetSessionCookie.ts";
import Config from "./Config.ts";
import SetDatonHeaders from "../http/kernel/module/SetDatonHeaders.ts";
@Unit()
export default class HttpKernel extends LifecycleUnit {
constructor(
protected readonly kernel: Kernel,
protected readonly config: Config,
) {
super()
}
public async up() {
PrepareRequest.register(this.kernel)
SetSessionCookie.register(this.kernel)
if ( this.config.get('server.powered_by.enable') ) {
SetDatonHeaders.register(this.kernel)
}
}
}

View File

@ -1,6 +1,6 @@
import { InstantiableCanonical } from './InstantiableCanonical.ts'
import { CanonicalDefinition } from './Canonical.ts'
import { Middleware } from '../http/Middleware.ts'
import Middleware from '../http/Middleware.ts'
import { Unit } from '../lifecycle/decorators.ts'
@Unit()

View File

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

16
lib/src/unit/Routes.ts Normal file
View File

@ -0,0 +1,16 @@
import {Canonical, CanonicalDefinition} from './Canonical.ts'
import {isRouterDefinition, RouterDefinition} from '../http/type/RouterDefinition.ts'
export default class Routes extends Canonical<RouterDefinition> {
protected base_path = './app/http/routes'
protected canonical_item = 'route_group'
protected suffix = '.routes.ts'
public async init_canonical_item(def: CanonicalDefinition): Promise<RouterDefinition> {
const item = await super.init_canonical_item(def)
if ( !isRouterDefinition(item) ) {
throw new TypeError(`Invalid routes definition: ${def.original_name}.`)
}
return item
}
}