diff --git a/app/bundle/daton_units.ts b/app/bundle/daton_units.ts index f052352..1057d93 100644 --- a/app/bundle/daton_units.ts +++ b/app/bundle/daton_units.ts @@ -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' diff --git a/app/configs/server.config.ts b/app/configs/server.config.ts index 91a21e0..a10b6bb 100644 --- a/app/configs/server.config.ts +++ b/app/configs/server.config.ts @@ -1,4 +1,9 @@ export default { port: 8080, use_ssl: false, + + powered_by: { + enable: true, + text: 'Daton', + }, } diff --git a/app/http/middleware/Test.middleware.ts b/app/http/middleware/Test.middleware.ts new file mode 100644 index 0000000..e752c7a --- /dev/null +++ b/app/http/middleware/Test.middleware.ts @@ -0,0 +1,5 @@ +import Middleware from '../../../lib/src/http/Middleware.ts' + +export default class TestMiddleware extends Middleware { + +} diff --git a/app/units.ts b/app/units.ts index 1bb5f37..36a5bd3 100644 --- a/app/units.ts +++ b/app/units.ts @@ -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, ] diff --git a/lib/src/collection/Collection.ts b/lib/src/collection/Collection.ts index b10c64d..9babea8 100755 --- a/lib/src/collection/Collection.ts +++ b/lib/src/collection/Collection.ts @@ -205,6 +205,17 @@ class Collection { return new Collection(this._items.filter(func)) } + find(func: KeyFunction): 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(bool: boolean, then: CollectionFunction): Collection { if ( bool ) then(this) return this diff --git a/lib/src/external/std.ts b/lib/src/external/std.ts index 1cd766c..644a0dc 100644 --- a/lib/src/external/std.ts +++ b/lib/src/external/std.ts @@ -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' diff --git a/lib/src/http/Controller.ts b/lib/src/http/Controller.ts index b4612b4..69bd5fe 100644 --- a/lib/src/http/Controller.ts +++ b/lib/src/http/Controller.ts @@ -1,4 +1,5 @@ +import AppClass from '../lifecycle/AppClass.ts' -export default class Controller { +export default class Controller extends AppClass { } diff --git a/lib/src/http/CookieJar.ts b/lib/src/http/CookieJar.ts index 0723849..3873b84 100644 --- a/lib/src/http/CookieJar.ts +++ b/lib/src/http/CookieJar.ts @@ -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,13 +24,18 @@ export class CookieJar { this._parsed = getCookies(this.request.to_native) } + public async get_raw(key: string): Promise { + return this._parsed[key] + } + public async get(key: string): Promise { // 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 ) { + if ( key in this._parsed ) { let value = this._parsed[key] try { value = JSON.parse(atob(this._parsed[key])) @@ -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 }) } diff --git a/lib/src/http/Middleware.ts b/lib/src/http/Middleware.ts index 019cf51..3d26ad0 100644 --- a/lib/src/http/Middleware.ts +++ b/lib/src/http/Middleware.ts @@ -1,3 +1,5 @@ -export class Middleware { +import AppClass from '../lifecycle/AppClass.ts' + +export default class Middleware extends AppClass { } diff --git a/lib/src/http/Response.ts b/lib/src/http/Response.ts index 630355b..d01513d 100644 --- a/lib/src/http/Response.ts +++ b/lib/src/http/Response.ts @@ -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 {HTTPResponse} from './type/HTTPResponse.ts' +import {HTTPRequest} from './type/HTTPRequest.ts' +import {ServerRequest} from '../external/http.ts' +import {CookieJar} from './CookieJar.ts' export class Response implements HTTPResponse { public status = 200 diff --git a/lib/src/http/kernel/Kernel.ts b/lib/src/http/kernel/Kernel.ts new file mode 100644 index 0000000..273da7d --- /dev/null +++ b/lib/src/http/kernel/Kernel.ts @@ -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) => Kernel, + after: (other?: Instantiable) => 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 = new Collection() + protected postflight: Collection = new Collection() + + public async handle(request: Request): Promise { + 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): ModuleRegistrationFluency { + return { + before: (other?: Instantiable): 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): 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 + }, + } + } +} \ No newline at end of file diff --git a/lib/src/http/kernel/Module.ts b/lib/src/http/kernel/Module.ts new file mode 100644 index 0000000..87eef84 --- /dev/null +++ b/lib/src/http/kernel/Module.ts @@ -0,0 +1,16 @@ +import {Request} from '../Request.ts' +import Kernel from './Kernel.ts' + +export default class Module { + public async match(request: Request): Promise { + return true + } + + public async apply(request: Request): Promise { + return request + } + + public static register(kernel: Kernel) { + kernel.register(this).before() + } +} diff --git a/lib/src/http/kernel/module/PrepareRequest.ts b/lib/src/http/kernel/module/PrepareRequest.ts new file mode 100644 index 0000000..dafcd81 --- /dev/null +++ b/lib/src/http/kernel/module/PrepareRequest.ts @@ -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 { + await request.prepare() + return request + } + +} diff --git a/lib/src/http/kernel/module/SetDatonHeaders.ts b/lib/src/http/kernel/module/SetDatonHeaders.ts new file mode 100644 index 0000000..009906c --- /dev/null +++ b/lib/src/http/kernel/module/SetDatonHeaders.ts @@ -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 { + const text = this.config.get('server.powered_by.text', 'Daton') + request.response.headers.set('X-Powered-By', text) + return request + } +} diff --git a/lib/src/http/kernel/module/SetSessionCookie.ts b/lib/src/http/kernel/module/SetSessionCookie.ts new file mode 100644 index 0000000..646cbd7 --- /dev/null +++ b/lib/src/http/kernel/module/SetSessionCookie.ts @@ -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 { + if ( !(await request.cookies.has('daton.session')) ) { + await request.cookies.set('daton.session', `${this.utility.uuid()}-${this.utility.uuid()}`) + } + return request + } + +} diff --git a/lib/src/http/type/RouterDefinition.ts b/lib/src/http/type/RouterDefinition.ts new file mode 100644 index 0000000..b3852fd --- /dev/null +++ b/lib/src/http/type/RouterDefinition.ts @@ -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 +} diff --git a/lib/src/lifecycle/AppClass.ts b/lib/src/lifecycle/AppClass.ts new file mode 100644 index 0000000..d530038 --- /dev/null +++ b/lib/src/lifecycle/AppClass.ts @@ -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(target: Instantiable|DependencyKey, ...parameters: any[]) { + return make(target, ...parameters) + } + + protected static get app() { + return make(Application) + } + + protected make(target: Instantiable|DependencyKey, ...parameters: any[]) { + return make(target, ...parameters) + } + + protected get app() { + return make(Application) + } +} diff --git a/lib/src/lifecycle/Unit.ts b/lib/src/lifecycle/Unit.ts index c17c13a..50e6e49 100644 --- a/lib/src/lifecycle/Unit.ts +++ b/lib/src/lifecycle/Unit.ts @@ -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() } - - protected make(target: Instantiable|DependencyKey, ...parameters: any[]) { - return make(target, ...parameters) - } } diff --git a/lib/src/service/utility/Utility.ts b/lib/src/service/utility/Utility.ts index 5fb9745..e2bbcbe 100644 --- a/lib/src/service/utility/Utility.ts +++ b/lib/src/service/utility/Utility.ts @@ -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() + } } diff --git a/lib/src/unit/Canon.ts b/lib/src/unit/Canon.ts index 733aff4..55ce172 100644 --- a/lib/src/unit/Canon.ts +++ b/lib/src/unit/Canon.ts @@ -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 } = {} - 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(key: string): Canonical { + if ( !this.resources[key] ) throw new NoSuchCanonicalResolverKeyError(key) + return this.resources[key] as Canonical } - register_resource(scope: string, resolver: CanonicalResolver) { - if ( this.resources[scope] ) { - throw new DuplicateResolverKeyError(scope) - } - - this.resources[scope] = resolver + register_canonical(unit: Canonical) { + const key = unit.canonical_items + if ( this.resources[key] ) throw new DuplicateResolverKeyError(key) + this.resources[key] = unit } } diff --git a/lib/src/unit/Canonical.ts b/lib/src/unit/Canonical.ts index c6cc68e..01f259f 100644 --- a/lib/src/unit/Canonical.ts +++ b/lib/src/unit/Canonical.ts @@ -28,8 +28,7 @@ export class Canonical 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 { diff --git a/lib/src/unit/HttpKernel.ts b/lib/src/unit/HttpKernel.ts new file mode 100644 index 0000000..5b3400c --- /dev/null +++ b/lib/src/unit/HttpKernel.ts @@ -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) + } + } + +} diff --git a/lib/src/unit/Middlewares.ts b/lib/src/unit/Middlewares.ts index 3dd1bdc..5c515e6 100644 --- a/lib/src/unit/Middlewares.ts +++ b/lib/src/unit/Middlewares.ts @@ -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() diff --git a/lib/src/unit/RecursiveCanonical.ts b/lib/src/unit/RecursiveCanonical.ts index f46106d..8944616 100644 --- a/lib/src/unit/RecursiveCanonical.ts +++ b/lib/src/unit/RecursiveCanonical.ts @@ -1,12 +1,12 @@ import {Canonical} from './Canonical.ts' export class RecursiveCanonical extends Canonical { - 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 } } diff --git a/lib/src/unit/Routes.ts b/lib/src/unit/Routes.ts new file mode 100644 index 0000000..0ae6cec --- /dev/null +++ b/lib/src/unit/Routes.ts @@ -0,0 +1,16 @@ +import {Canonical, CanonicalDefinition} from './Canonical.ts' +import {isRouterDefinition, RouterDefinition} from '../http/type/RouterDefinition.ts' + +export default class Routes extends Canonical { + protected base_path = './app/http/routes' + protected canonical_item = 'route_group' + protected suffix = '.routes.ts' + + public async init_canonical_item(def: CanonicalDefinition): Promise { + const item = await super.init_canonical_item(def) + if ( !isRouterDefinition(item) ) { + throw new TypeError(`Invalid routes definition: ${def.original_name}.`) + } + return item + } +}