JSDoc all the things!

This commit is contained in:
2020-08-17 09:44:23 -05:00
parent c2a7c3f914
commit f67ae37923
121 changed files with 2855 additions and 63 deletions

View File

@@ -4,6 +4,10 @@ import ResponseFactory from './response/ResponseFactory.ts'
import * as api from '../support/api.ts'
import JSONResponseFactory from './response/JSONResponseFactory.ts'
/**
* HTTP controller which wraps its handlers output in JSON response factories, if appropriate.
* @extends Controller
*/
export default class ApiController extends Controller {
public get_bound_method(method_name: string): (...args: any[]) => any {
// @ts-ignore

View File

@@ -1,5 +1,9 @@
import AppClass from '../lifecycle/AppClass.ts'
/**
* Base class for an HTTP controller.
* @extends AppClass
*/
export default class Controller extends AppClass {
}

View File

@@ -2,32 +2,63 @@ 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";
/**
* Base type representing a parsed cookie.
*/
export interface Cookie {
key: string,
original_value: string,
value: any,
}
/**
* Type representing what might be a cookie, or might be undefined.
*/
export type MaybeCookie = Cookie | undefined
// TODO cookie options (http only, expires, &c.)
/**
* Base class for managing cookies.
*/
@Injectable()
export class CookieJar {
/**
* Cache of parsed cookie values.
* @type object
*/
protected _parsed: { [key: string]: string } = {}
/**
* Cache of cookie values.
* @type InMemCache
*/
protected _cache = new InMemCache()
constructor(
/**
* The associated request.
* @type HTTPRequest
*/
protected request: HTTPRequest,
) {
this._parsed = getCookies(this.request.to_native)
}
/**
* Get the raw value of a cookie string, if it is defined.
* @param {string} key
* @return string | undefined
*/
public async get_raw(key: string): Promise<string | undefined> {
return this._parsed[key]
}
/**
* Get the parsed value of a cookie, if it is defined.
* @param {string} key
* @return Promise<MaybeCookie>
*/
public async get(key: string): Promise<MaybeCookie> {
// Try the cache
if ( await this._cache.has(key) ) {
@@ -52,6 +83,12 @@ export class CookieJar {
}
}
/**
* Set the cookie for the given key to the serialized value.
* @param {string} key
* @param value
* @return Promise<void>
*/
public async set(key: string, value: any): Promise<void> {
const original_value = btoa(JSON.stringify(value))
const cookie = {
@@ -64,10 +101,20 @@ export class CookieJar {
setCookie(this.request.response, { name: key, value: original_value })
}
/**
* Returns true if the given cookie exists.
* @param {string} key
* @return Promise<boolean>
*/
public async has(key: string): Promise<boolean> {
return (await this._cache.has(key)) || key in this._parsed
}
/**
* Deletes the given cookie, if it exists.
* @param {string} key
* @return Promise<void>
*/
public async delete(key: string): Promise<void> {
await this._cache.drop(key)
delCookie(this.request.response, key)

View File

@@ -1,5 +1,9 @@
import AppClass from '../lifecycle/AppClass.ts'
/**
* Base class for HTTP middleware.
* @extends AppClass
*/
export default class Middleware extends AppClass {
}

View File

@@ -7,46 +7,129 @@ import { Injectable } from '../../../di/src/decorator/Injection.ts'
import SessionInterface from './session/SessionInterface.ts'
import ActivatedRoute from './routing/ActivatedRoute.ts'
/**
* Base class for Daton-managed HTTP requests.
* @implements HTTPRequest
*/
@Injectable()
export class Request implements HTTPRequest {
/**
* The associated response object.
* @type HTTPResponse
*/
public readonly response: HTTPResponse
/**
* The base raw Deno request.
* @type ServerRequest
*/
private readonly _deno_req: ServerRequest
/**
* The parsed body.
*/
private _body: any
/**
* The parsed query params.
* @type object
*/
private _query: { [key: string]: any } = {}
/**
* The associated session.
* @type SessionInterface
*/
private _session!: SessionInterface
/**
* The matched, mounted route.
* @type ActivatedRoute
*/
private _activated_route!: ActivatedRoute
/**
* The incoming URL.
* @type string
*/
public readonly url: string
/**
* The incoming request method.
* @type string
*/
public readonly method: string
/**
* The incoming HTTP protocol.
* @type HTTPProtocol
*/
public readonly protocol: HTTPProtocol
/**
* The underlying Deno connection.
* @type Deno.Conn
*/
public readonly connection: Deno.Conn
/**
* True if the request used HTTPS.
* @type boolean
*/
public readonly secure: boolean = false
/**
* The incoming headers.
*/
public get headers() {
return this._deno_req.headers
}
/**
* Get the underlying Deno request.
* @type ServerRequest
*/
get to_native(): ServerRequest {
return this._deno_req
}
/**
* Get the cookies for the response.
* @type CookieJar
*/
get cookies() {
return this.response.cookies
}
/**
* Get the associated session.
* @type SessionInterface
*/
get session(): SessionInterface {
return this._session
}
/**
* Set the associated session.
* @param {SessionInterface} session
*/
set session(session: SessionInterface) {
if ( !this._session )
this._session = session
}
/**
* Get the activated route mounted to this request.
* @type ActivatedRoute
*/
get route(): ActivatedRoute {
return this._activated_route
}
/**
* Mount the given route to the request.
* @param {ActivatedRoute} route
*/
set route(route: ActivatedRoute) {
if ( !this._activated_route )
this._activated_route = route
@@ -54,6 +137,9 @@ export class Request implements HTTPRequest {
constructor(
protected utility: Utility,
/**
* The raw Deno request.
*/
from: ServerRequest
) {
this._deno_req = from
@@ -68,6 +154,10 @@ export class Request implements HTTPRequest {
this.response = new Response(this)
}
/**
* Prepare the request for the Daton framework.
* @return Promise<void>
*/
public async prepare() {
this._body = await Deno.readAll(this._deno_req.body)
@@ -83,6 +173,10 @@ export class Request implements HTTPRequest {
this._query = params
}
/**
* Send the given response.
* @param res
*/
respond(res: any) {
return this._deno_req.respond(res)
}
@@ -90,26 +184,50 @@ export class Request implements HTTPRequest {
// public body: RequestBody = {}
// public original_body: RequestBody = {}
/**
* Get the remote host information.
* @type RemoteHost
*/
get remote() {
return this.connection.remoteAddr as RemoteHost
}
/**
* Get the raw body from the request.
* @type string
*/
get body() {
return this._body
}
/**
* Get the query params.
* @type object
*/
get query() {
return this._query
}
/**
* Get the incoming host name.
* @type string
*/
get hostname() {
return this.headers.get('host')?.split(':')[0]
}
/**
* Get the incoming path of the route.
* @type string
*/
get path() {
return this.url.split('?')[0].split('#')[0]
}
/**
* True if the request is an XHR incoming.
* @type boolean
*/
get xhr() {
return this.headers.get('x-requested-with')?.toLowerCase() === 'xmlhttprequest'
}

View File

@@ -3,26 +3,74 @@ import {HTTPRequest} from './type/HTTPRequest.ts'
import {ServerRequest} from '../external/http.ts'
import {CookieJar} from './CookieJar.ts'
/**
* Base class for a Daton-managed response.
* @implements HTTPResponse
*/
export class Response implements HTTPResponse {
/**
* The outgoing HTTP status.
* @type HTTPStatus
*/
public status = 200
/**
* The response headers.
* @type Headers
*/
public headers = new Headers()
/**
* The response body.
* @type string
*/
public body = ''
/**
* The cookie manager.
* @type CookieJar
*/
public readonly cookies: CookieJar
/**
* The raw Deno request
* @type ServerRequest
*/
private readonly _deno_req: ServerRequest
/**
* The associated Daton request.
* @type HTTPRequest
*/
private readonly _request: HTTPRequest
/**
* True if the response has been sent.
* @type boolean
*/
private _sent = false
/**
* True if the response has been send.
* @type boolean
*/
get sent() {
return this._sent
}
/**
* Create a new response
* @param {HTTPRequest} to - the associated request
*/
constructor(to: HTTPRequest) {
this._deno_req = to.to_native
this._request = to
this.cookies = new CookieJar(to)
}
/**
* Send the response.
*/
send() {
this._sent = true
return this._deno_req.respond(this)

View File

@@ -1,6 +1,11 @@
import { HTTPRequest } from './type/HTTPRequest.ts'
import { Request } from './Request.ts'
/**
* Base class for requests made with HTTPS.
* @extends Request
* @implements HTTPRequest
*/
export default class SecureRequest extends Request implements HTTPRequest {
public readonly secure: boolean = true
}

View File

@@ -2,8 +2,16 @@ import ResponseFactory from './ResponseFactory.ts'
import {Rehydratable} from '../../support/Rehydratable.ts'
import {Request} from '../Request.ts'
/**
* Response factory that returns a JSON object of the state of a rehydratable object.
* @extends ResponseFactory
*/
export default class DehydratedStateResponseFactory extends ResponseFactory {
constructor(
/**
* The object to dehydrate.
* @type Rehydratable
*/
public readonly rehydratable: Rehydratable
) {
super()

View File

@@ -3,12 +3,32 @@ import {Request} from '../Request.ts'
import * as api from '../../support/api.ts'
import {HTTPStatus} from '../../const/http.ts'
/**
* Response factory to render a handled request-level error.
* @extends ResponseFactory
*/
export default class ErrorResponseFactory extends ResponseFactory {
/**
* The target output mode.
* @type 'json' | 'html'
*/
protected target_mode: 'json' | 'html' = 'html'
constructor(
/**
* The error to display.
* @type Error
*/
public readonly error: Error,
/**
* THe HTTP status to use for the response.
* @type HTTPStatus
*/
status: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR,
/**
* The output format.
* @type 'json' | 'html'
*/
output: 'json' | 'html' = 'html',
) {
super()
@@ -16,11 +36,21 @@ export default class ErrorResponseFactory extends ResponseFactory {
this.mode(output)
}
/**
* Override the output mode.
* @param {'json' | 'html'} output
* @return ErrorResponseFactory
*/
public mode(output: 'json' | 'html'): ErrorResponseFactory {
this.target_mode = output
return this
}
/**
* Write this response factory to the given request's response.
* @param {Request} request
* @return Request
*/
public async write(request: Request): Promise<Request> {
request = await super.write(request)
@@ -35,6 +65,11 @@ export default class ErrorResponseFactory extends ResponseFactory {
return request
}
/**
* Build the HTML display for the given error.
* @param {Error} error
* @return string
*/
protected build_html(error: Error) {
return `
<b>Sorry, an unexpected error occurred while processing your request.</b>

View File

@@ -1,6 +1,10 @@
import ResponseFactory from './ResponseFactory.ts'
import {Request} from '../Request.ts'
/**
* Response factory that writes a string as HTML.
* @extends ResponseFactory
*/
export default class HTMLResponseFactory extends ResponseFactory {
constructor(
public readonly value: string,

View File

@@ -1,9 +1,21 @@
import ErrorResponseFactory from './ErrorResponseFactory.ts'
import HTTPError from '../../error/HTTPError.ts'
/**
* Response factory that renders a given HTTP error.
* @extends ErrorResponseFactory
*/
export default class HTTPErrorResponseFactory extends ErrorResponseFactory {
constructor(
/**
* The HTTP error to render.
* @type HTTPError
*/
public readonly error: HTTPError,
/**
* The output format.
* @type 'json' | 'html'
*/
output: 'json' | 'html',
) {
super(error, error.http_status, output)

View File

@@ -1,8 +1,15 @@
import ResponseFactory from './ResponseFactory.ts'
import {Request} from '../Request.ts'
/**
* Response factory that writes the given value as JSON.
* @extends ResponseFactory
*/
export default class JSONResponseFactory extends ResponseFactory {
constructor(
/**
* The value to be JSON serialized and written.
*/
public readonly value: any
) {
super()

View File

@@ -2,9 +2,20 @@ import ResponseFactory from './ResponseFactory.ts'
import ViewEngine from '../../unit/ViewEngine.ts'
import {Request} from '../Request.ts'
/**
* Response factory that renders a partial view as HTML.
* @return ResponseFactory
*/
export default class PartialViewResponseFactory extends ResponseFactory {
constructor(
/**
* The view name.
* @type string
*/
public readonly view: string,
/**
* Optionally, the response context.
*/
public readonly context?: any,
) {
super()

View File

@@ -2,14 +2,32 @@ import AppClass from '../../lifecycle/AppClass.ts'
import {Request} from '../Request.ts'
import {HTTPStatus} from '../../const/http.ts'
/**
* A base class that renders a response to a request.
* @extends AppClass
*/
export default abstract class ResponseFactory extends AppClass {
/**
* The HTTP status to set on the response.
* @type HTTPStatus
*/
protected target_status: HTTPStatus = HTTPStatus.OK
/**
* Write the value to the response.
* @param {Request} request
* @return Promise<Request>
*/
public async write(request: Request): Promise<Request> {
request.response.status = this.target_status
return request
}
/**
* Override the response status.
* @param {HTTPStatus} status
* @return ResponseFactory
*/
public status(status: HTTPStatus): ResponseFactory {
this.target_status = status
return this

View File

@@ -1,8 +1,16 @@
import ResponseFactory from './ResponseFactory.ts'
import {Request} from '../Request.ts'
/**
* Response factory that renders the given value as a string.
* @return ResponseFactory
*/
export default class StringResponseFactory extends ResponseFactory {
constructor(
/**
* Value to be written.
* @type string
*/
public readonly value: string,
) {
super()

View File

@@ -2,10 +2,18 @@ import ResponseFactory from './ResponseFactory.ts'
import {Request} from '../Request.ts'
import {HTTPStatus} from '../../const/http.ts'
/**
* Response factory that sends a temporary redirect.
* @extends ResponseFactory
*/
export default class TemporaryRedirectResponseFactory extends ResponseFactory {
protected target_status: HTTPStatus = HTTPStatus.TEMPORARY_REDIRECT
constructor(
/**
* Destination to redirect the user to.
* @type string
*/
public readonly destination: string,
) {
super()

View File

@@ -2,10 +2,25 @@ import ResponseFactory from './ResponseFactory.ts'
import ViewEngine from '../../unit/ViewEngine.ts'
import {Request} from '../Request.ts'
/**
* Response factory that renders the given view as HTML.
* @extends ResponseFactory
*/
export default class ViewResponseFactory extends ResponseFactory {
constructor(
/**
* The view name.
* @type string
*/
public readonly view: string,
/**
* Optionally, the view context.
*/
public readonly context?: any,
/**
* Optionally, the layout name.
* @type string
*/
public readonly layout?: string,
) {
super()

View File

@@ -11,35 +11,80 @@ import HTTPError from '../../error/HTTPError.ts'
import ViewResponseFactory from './ViewResponseFactory.ts'
import PartialViewResponseFactory from './PartialViewResponseFactory.ts'
/**
* Get a new JSON response factory that writes the given object as JSON.
* @param value
* @return JSONResponseFactory
*/
export function json(value: any): JSONResponseFactory {
return make(JSONResponseFactory, value)
}
/**
* Get a new HTML response factory that writes the given string as HTML.
* @param value
* @return HTMLResponseFactory
*/
export function html(value: string): HTMLResponseFactory {
return make(HTMLResponseFactory, value)
}
/**
* Get a new Error response factory that writes the given error.
* @param {Error|string} error
* @param {number} [status = 500] - the HTTP response status
* @return ErrorResponseFactory
*/
export function error(error: Error | string, status: number = 500): ErrorResponseFactory {
if ( typeof error === 'string' ) error = new Error(error)
return make(ErrorResponseFactory, error, status)
}
/**
* Get a new dehydrated response factory that dehydrates the given object and writes its state as JSON.
* @param {Rehydratable} value
* @return DehydratedStateResponseFactory
*/
export function dehydrate(value: Rehydratable): DehydratedStateResponseFactory {
return make(DehydratedStateResponseFactory, value)
}
/**
* Get a new temporary redirect response factory that redirects to the given destination.
* @param {string} destination
* @return TemporaryRedirectResponseFactory
*/
export function redirect(destination: string): TemporaryRedirectResponseFactory {
return make(TemporaryRedirectResponseFactory, destination)
}
/**
* Get a new http error response factory for the given http status code.
* @param {HTTPStatus} status
* @param {string} [message]
* @return HTTPErrorResponseFactory
*/
export function http(status: HTTPStatus, message?: string): HTTPErrorResponseFactory {
return make(HTTPErrorResponseFactory, new HTTPError(status, message))
}
/**
* Get a new view response factory for the given view name, passing along context and layout.
* @param {string} view
* @param [context]
* @param {string} [layout]
* @return ViewResponseFactory
*/
export function view(view: string, context?: any, layout?: string): ViewResponseFactory {
return make(ViewResponseFactory, view, context, layout)
}
/**
* Get a new partial view response factory for the given view name, passing along context.
* @param {string} view
* @param [context]
* @return PartialViewResponseFactory
*/
export function partial(view: string, context?: any): PartialViewResponseFactory {
return make(PartialViewResponseFactory, view, context)
}

View File

@@ -2,12 +2,32 @@ import AppClass from '../../lifecycle/AppClass.ts'
import {Route, RouteParameters} from './Route.ts'
import {RouteHandlers} from '../../unit/Routing.ts'
/**
* Base class representing a route mounted to a request.
* @extends AppClass
*/
export default class ActivatedRoute extends AppClass {
/**
* The incoming parameters parsed from the route.
* @type RouteParameters
*/
public readonly params: RouteParameters
constructor(
/**
* The incoming route path string.
* @type string
*/
public readonly incoming: string,
/**
* The matched route.
* @type Route
*/
public readonly route: Route,
/**
* The handlers for this route.
* @type RouteHandlers | undefined
*/
public readonly handlers: RouteHandlers | undefined,
) {
super()

View File

@@ -2,6 +2,10 @@ import {Route, RouteParameters, RouteSegment} from './Route.ts'
import Utility from '../../service/utility/Utility.ts'
import {make} from '../../../../di/src/global.ts'
/**
* A route that contains route parameters and shallow wild-cards.
* @extends Route
*/
export class ComplexRoute extends Route {
public match(incoming: string): boolean {
const base_parts = this.split(this.base)

View File

@@ -1,9 +1,21 @@
import {Route, RouteParameters} from './Route.ts'
/**
* A route that contains deep wild-cards.
* @extends Route
*/
export class DeepmatchRoute extends Route {
/**
* The built regex for parsing the route.
* @type RegExp
*/
protected base_regex: RegExp
constructor(
/**
* Base route definition.
* @type string
*/
protected base: string
) {
super(base)
@@ -30,6 +42,10 @@ export class DeepmatchRoute extends Route {
return params
}
/**
* Build the regex object for the given route parts.
* @param {Array<string>} base_parts
*/
protected build_regex(base_parts: string[]) {
const deepmatch_group = '([a-zA-Z0-9\\-\\_\\.\\/]+)' // allows for alphanum, -, _, ., and /
const shallowmatch_group = '([a-zA-Z0-9\\-\\.\\_]+)' // allows for alphanum, -, ., and _

View File

@@ -2,11 +2,27 @@ import {Route, RouteParameters} from './Route.ts'
import {Logging} from '../../service/logging/Logging.ts'
import {make} from '../../../../di/src/global.ts'
/**
* Route that is defined and matched by regex
* @extends Route
*/
export class RegExRoute extends Route {
/**
* Generated regex for the definition.
* @type RegExp
*/
protected key_regex: RegExp
constructor(
/**
* Route base string.
* @type string
*/
protected base: string,
/**
* Regex key.
* @type string
*/
protected key: string,
) {
super(base)
@@ -48,6 +64,11 @@ export class RegExRoute extends Route {
return params
}
/**
* Build the regex for the given route, from its parsed key.
* @param {string} key
* @return RegExp
*/
protected build_regex(key: string) {
if ( !key.startsWith('rex ') ) {
throw new TypeError(`Invalid regular expression route pattern: ${key}`)

View File

@@ -1,25 +1,56 @@
import {logger} from "../../service/logging/global.ts";
import {logger} from '../../service/logging/global.ts'
export type RouteParameters = { [key: string]: string }
export type RouteSegment = { base: string, match: string | undefined }
export type ZippedRouteSegments = RouteSegment[]
/**
* Abstract base class representing a parsed and loaded route.
*/
export abstract class Route {
constructor(
/**
* The base definition string.
* @type string
*/
protected base: string
) { }
/**
* Given an incoming route path, returns true if that route matches this route definition.
* @param {string} incoming
* @return boolean
*/
public abstract match(incoming: string): boolean
/**
* Given an incoming route path, parse the parameters and return them.
* @param {string} incoming
* @return RouteParameters
*/
public abstract build_parameters(incoming: string): RouteParameters
/**
* Get the base definition of this route.
*/
public get route() {
return this.base
}
/**
* Split the given route string into its segments by '/'.
* @param {string} incoming
* @return {Array<string>}
*/
public split(incoming: string) {
return incoming.toLowerCase().split('/')
}
/**
* Split the incoming route segment and match each segment with the corresponding segment of the definition.
* @param {string} incoming
* @return ZippedRouteSegments
*/
public zip(incoming: string) {
const incoming_parts: string[] = this.split(incoming)
const base_parts: string[] = this.split(this.base)

View File

@@ -1,5 +1,9 @@
import {Route, RouteParameters} from './Route.ts'
/**
* A very basic route with no parameters or wild-cards.
* @extends Route
*/
export class SimpleRoute extends Route {
public match(incoming: string): boolean {
return incoming.toLowerCase() === this.base.toLowerCase()

View File

@@ -1,6 +1,11 @@
import Session from './Session.ts'
import SessionInterface, { SessionData } from './SessionInterface.ts'
/**
* Basic session implementation that exists only in memory.
* @extends Session
* @implements SessionInterface
*/
export default class MemorySession extends Session implements SessionInterface {
private _key!: string
private _data: SessionData = {}

View File

@@ -2,6 +2,10 @@ import SessionFactory from './SessionFactory.ts'
import MemorySession from './MemorySession.ts'
import SessionInterface from './SessionInterface.ts'
/**
* Session factory that produces memory-based sessions.
* @extends SessionFactory
*/
export default class MemorySessionFactory extends SessionFactory {
produce(dependencies: any[], parameters: any[]): SessionInterface {
return new MemorySession()

View File

@@ -5,10 +5,21 @@ import SessionManager, {InvalidSessionKeyError} from './SessionManager.ts'
import Utility from '../../service/utility/Utility.ts'
import SessionInterface from './SessionInterface.ts'
/**
* Type denoting a memory-stored session.
*/
export type SessionRegistrant = { key: string, session: SessionInterface }
/**
* Session manager object for memory-based sessions.
* @extends SessionManager
*/
@Service()
export default class MemorySessionManager extends SessionManager {
/**
* Collection of registered, in-memory sessions.
* @type Collection<SessionRegistrant>
*/
private _sessions: Collection<SessionRegistrant> = new Collection<SessionRegistrant>()
public async has_session(key: string): Promise<boolean> {

View File

@@ -1,6 +1,10 @@
import SessionManagerFactory from "./SessionManagerFactory.ts";
import MemorySessionManager from "./MemorySessionManager.ts";
import SessionManagerFactory from './SessionManagerFactory.ts'
import MemorySessionManager from './MemorySessionManager.ts'
/**
* Session manager factory that produces memory-based session managers.
* @extends SessionManagerFactory
*/
export default class MemorySessionManagerFactory extends SessionManagerFactory {
produce(dependencies: any[], parameters: any[]): any {
return new MemorySessionManager()

View File

@@ -4,8 +4,16 @@ import {Model} from '../../../../orm/src/model/Model.ts'
import {StaticClass} from '../../../../di/src/type/StaticClass.ts'
import {isInstantiable} from '../../../../di/src/type/Instantiable.ts'
/**
* Session factory that builds an ORM model-based session factory.
* @extends SessionFactory
*/
export default class ModelSessionFactory extends SessionFactory {
constructor(
/**
* The base model to use for sessions.
* @type StaticClass<SessionInterface, typeof Model>
*/
protected readonly ModelClass: StaticClass<SessionInterface, typeof Model>,
) {
super()

View File

@@ -3,8 +3,16 @@ import {Model} from '../../../../orm/src/model/Model.ts'
import SessionInterface, {isSessionInterface} from './SessionInterface.ts'
import {StaticClass} from '../../../../di/src/type/StaticClass.ts'
/**
* Session manager that manages sessions using an ORM model.
* @extends SessionManager
*/
export default class ModelSessionManager extends SessionManager {
constructor(
/**
* The base model class to use for session lookups.
* @type StaticClass<SessionInterface, typeof Model>
*/
protected readonly ModelClass: StaticClass<SessionInterface, typeof Model>,
) {
super()

View File

@@ -4,8 +4,16 @@ import {Model} from '../../../../orm/src/model/Model.ts'
import {StaticClass} from '../../../../di/src/type/StaticClass.ts'
import SessionInterface from './SessionInterface.ts'
/**
* Session manager factory that produces model-based session managers.
* @extends SessionManagerFactory
*/
export default class MemorySessionManagerFactory extends SessionManagerFactory {
constructor(
/**
* The base model class to use for session lookups.
* @type StaticClass<SessionInterface, typeof Model>
*/
protected readonly ModelClass: StaticClass<SessionInterface, typeof Model>,
) {
super()

View File

@@ -1,13 +1,59 @@
import AppClass from '../../lifecycle/AppClass.ts'
import SessionInterface, {SessionData} from './SessionInterface.ts'
/**
* Abstract base-class for the request's session.
* @extends AppClass
* @implements SessionInterface
*/
export default abstract class Session extends AppClass implements SessionInterface {
/**
* Get the unique identifier for this session.
* @return string
*/
public abstract get_key(): string
/**
* Set the unique identifier for this session.
* @param {string} key
*/
public abstract set_key(key: string): void
/**
* Persist the session to its storage backend.
* @return Promise<void>
*/
public abstract async persist(): Promise<void>
/**
* Get the session data.
* @return SessionData
*/
public abstract get_data(): SessionData
/**
* Set the session data.
* @param {SessionData} data
*/
public abstract set_data(data: SessionData): void
/**
* Get the session attribute by key.
* @param {string} key
* @return any
*/
public abstract get_attribute(key: string): any
/**
* Set the session attribute by key.
* @param {string} key
* @param {any} value
*/
public abstract set_attribute(key: string, value: any): void
/**
* Initialize the session in its backend.
* @return Promise<void>
*/
public abstract async init_session(): Promise<void>
}

View File

@@ -5,8 +5,10 @@ import {DependencyRequirement} from '../../../../di/src/type/DependencyRequireme
import {Collection} from '../../collection/Collection.ts'
import SessionInterface from './SessionInterface.ts'
// TODO support configurable session backends
/**
* Base class for IoC container factories that produce sessions.
* @extends AbstractFactory
*/
export default class SessionFactory extends AbstractFactory {
constructor() {
super({})

View File

@@ -1,7 +1,13 @@
import {logger} from "../../service/logging/global.ts";
import {logger} from '../../service/logging/global.ts'
/**
* Base type for session data.
*/
export type SessionData = { [key: string]: any }
/**
* Base type for the abstract session interface.
*/
export default interface SessionInterface {
get_key(): string
set_key(key: string): void
@@ -13,6 +19,11 @@ export default interface SessionInterface {
init_session(): Promise<void>
}
/**
* Returns true if the given object is a valid session.
* @param what
* @return boolean
*/
export function isSessionInterface(what: any): what is SessionInterface {
const name_length_checks = [
{ name: 'get_key', length: 0 },

View File

@@ -1,16 +1,41 @@
import AppClass from '../../lifecycle/AppClass.ts'
import SessionInterface from './SessionInterface.ts'
/**
* Error thrown if a session is looked up using a key that doesn't exist.
* @extends Error
*/
export class InvalidSessionKeyError extends Error {
constructor(key: any) {
super(`Invalid session key: ${key}. No session exists.`)
}
}
/**
* Abstract class for managing sessions.
* @extends AppClass
*/
export default abstract class SessionManager extends AppClass {
/**
* Attempt to find a session by key if it exists, or create one if no key is provided.
* @param {string} [key]
* @return Promise<SessionInterface>
*/
public abstract async get_session(key?: string): Promise<SessionInterface>
/**
* Returns true if the manager has a session with the given key.
* @param {string} key
* @return Promise<boolean>
*/
public abstract async has_session(key: string): Promise<boolean>
/**
* Purge a session by key, if provided, or all sessions.
* @param {string} key
* @return Promise<void>
*/
public abstract async purge(key?: string): Promise<void>
}

View File

@@ -4,8 +4,10 @@ import {Collection} from '../../collection/Collection.ts'
import MemorySessionManager from './MemorySessionManager.ts'
import SessionManager from './SessionManager.ts'
// TODO support configurable session backends
/**
* Base class for IoC factories that produce session managers.
* @extends AbstractFactory
*/
export default class SessionManagerFactory extends AbstractFactory {
constructor() {
super({})

View File

@@ -3,9 +3,18 @@ import SessionInterface, {SessionData} from './SessionInterface.ts'
import {Field} from '../../../../orm/src/model/Field.ts'
import {Type} from '../../../../orm/src/db/types.ts'
/**
* Base class for an ORM session model.
* @extends Model<SessionModel>
* @implements SessionInterface
*/
export default class SessionModel extends Model<SessionModel> implements SessionInterface {
protected static populate_key_on_insert: boolean = true
/**
* The JSON serialized session data.
* @type string
*/
@Field(Type.json)
protected data?: string

View File

@@ -3,18 +3,27 @@ import {HTTPResponse} from './HTTPResponse.ts'
import SessionInterface from '../session/SessionInterface.ts'
import ActivatedRoute from '../routing/ActivatedRoute.ts'
/**
* Base type representing an HTTP protocol version.
*/
export interface HTTPProtocol {
string: string,
major: number,
minor: number,
}
/**
* Base type representing a remote host.
*/
export interface RemoteHost {
hostname: string,
port: number,
transport: string,
}
/**
* Base type for an incoming HTTP request.
*/
export interface HTTPRequest {
url: string
method: string

View File

@@ -1,5 +1,8 @@
import {CookieJar} from '../CookieJar.ts'
/**
* Base type for an outgoing HTTP response.
*/
export interface HTTPResponse {
status: number
headers: Headers

View File

@@ -1,8 +1,18 @@
import {logger} from '../../service/logging/global.ts'
/**
* Type representing valid HTTP verbs.
*/
export type RouteVerb = 'get' | 'post' | 'patch' | 'delete' | 'head' | 'put' | 'connect' | 'options' | 'trace'
/**
* Type representing a route verb group from a router definition.
*/
export type RouteVerbGroup = { [key: string]: string | string[] }
/**
* Type representing a router definition.
*/
export interface RouterDefinition {
prefix?: string,
middleware?: string[],
@@ -17,11 +27,21 @@ export interface RouterDefinition {
trace?: RouteVerbGroup,
}
/**
* Returns true if the given value is a valid HTTP verb.
* @param something
* @return boolean
*/
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)
}
/**
* Returns true if the given value is a valid route verb group definition.
* @param something
* @return boolean
*/
export function isRouteVerbGroup(something: any): something is RouteVerbGroup {
if ( !(typeof something === 'object' ) ) return false
for ( const key in something ) {
@@ -41,6 +61,11 @@ export function isRouteVerbGroup(something: any): something is RouteVerbGroup {
return true
}
/**
* Returns true if the given value is a valid router definition.
* @param something
* @return boolean
*/
export function isRouterDefinition(something: any): something is RouterDefinition {
if ( !(typeof something === 'object') ) {
logger.debug('Routing definition is not an object.')