JSDoc all the things!
This commit is contained in:
parent
c2a7c3f914
commit
f67ae37923
@ -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
|
||||
|
@ -1,5 +1,9 @@
|
||||
import AppClass from '../lifecycle/AppClass.ts'
|
||||
|
||||
/**
|
||||
* Base class for an HTTP controller.
|
||||
* @extends AppClass
|
||||
*/
|
||||
export default class Controller extends AppClass {
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -1,5 +1,9 @@
|
||||
import AppClass from '../lifecycle/AppClass.ts'
|
||||
|
||||
/**
|
||||
* Base class for HTTP middleware.
|
||||
* @extends AppClass
|
||||
*/
|
||||
export default class Middleware extends AppClass {
|
||||
|
||||
}
|
||||
|
@ -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'
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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 _
|
||||
|
@ -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}`)
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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 = {}
|
||||
|
@ -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()
|
||||
|
@ -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> {
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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({})
|
||||
|
@ -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 },
|
||||
|
@ -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>
|
||||
|
||||
}
|
||||
|
@ -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({})
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -1,5 +1,8 @@
|
||||
import {CookieJar} from '../CookieJar.ts'
|
||||
|
||||
/**
|
||||
* Base type for an outgoing HTTP response.
|
||||
*/
|
||||
export interface HTTPResponse {
|
||||
status: number
|
||||
headers: Headers
|
||||
|
@ -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.')
|
||||
|
@ -3,10 +3,18 @@ import {DependencyKey} from '../../../di/src/type/DependencyKey.ts'
|
||||
import {make} from '../../../di/src/global.ts'
|
||||
import Application from '../lifecycle/Application.ts'
|
||||
|
||||
/**
|
||||
* Base type for a class that supports binding methods by string.
|
||||
*/
|
||||
export interface Bindable {
|
||||
get_bound_method(method_name: 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
|
||||
@ -16,23 +24,50 @@ export function isBindable(what: any): what is Bindable {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for Daton-interactive classes. Provides helpful utilities for accessing
|
||||
* the underlying application and IoC container.
|
||||
*/
|
||||
export default class AppClass {
|
||||
/**
|
||||
* Use the IoC container to create an instance of the given class.
|
||||
* @param {Instantiable|DependencyKey} target - the key to instantiate
|
||||
* @param {...any} parameters - parameters to pass to the constructor
|
||||
*/
|
||||
protected static make<T>(target: Instantiable<T>|DependencyKey, ...parameters: any[]) {
|
||||
return make(target, ...parameters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Daton app.
|
||||
* @type Application
|
||||
*/
|
||||
protected static get app() {
|
||||
return make(Application)
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the IoC container to create an instance of the given class.
|
||||
* @param {Instantiable|DependencyKey} target - the key to instantiate
|
||||
* @param {...any} parameters - parameters to pass to the constructor
|
||||
*/
|
||||
protected make<T>(target: Instantiable<T>|DependencyKey, ...parameters: any[]) {
|
||||
return make(target, ...parameters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Daton app.
|
||||
* @type Application
|
||||
*/
|
||||
protected get app() {
|
||||
return make(Application)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the method with the given name from this class, bound to this class.
|
||||
* @param {string} method_name
|
||||
* @return function
|
||||
*/
|
||||
public get_bound_method(method_name: string): (...args: any[]) => any {
|
||||
// @ts-ignore
|
||||
if ( typeof this[method_name] !== 'function' ) {
|
||||
|
@ -9,24 +9,47 @@ import Instantiable from '../../../di/src/type/Instantiable.ts'
|
||||
import {Collection} from '../collection/Collection.ts'
|
||||
import {path} from '../external/std.ts'
|
||||
|
||||
/**
|
||||
* Central class for Daton applications.
|
||||
*/
|
||||
@Service()
|
||||
export default class Application {
|
||||
/**
|
||||
* Collection of LifecycleUnits instantiated by this application.
|
||||
* @type Collection<LifecycleUnit>
|
||||
*/
|
||||
protected instantiated_units: Collection<LifecycleUnit> = new Collection<LifecycleUnit>()
|
||||
|
||||
constructor(
|
||||
protected logger: Logging,
|
||||
protected rleh: RunLevelErrorHandler,
|
||||
/**
|
||||
* Array of unit classes to run for this application.
|
||||
* @type Array<Instantiable<LifecycleUnit>>
|
||||
*/
|
||||
protected units: (Instantiable<LifecycleUnit>)[],
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Use the IoC container to instantiate the given dependency key.
|
||||
* @param {DependencyKey} token
|
||||
*/
|
||||
make(token: DependencyKey) {
|
||||
return make(token)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the IoC container.
|
||||
* @return Container
|
||||
*/
|
||||
container() {
|
||||
return container
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch the application.
|
||||
* @return Promise<void>
|
||||
*/
|
||||
async up() {
|
||||
this.logger.info('Starting Daton...', true)
|
||||
for ( const unit_class of this.units ) {
|
||||
@ -36,10 +59,18 @@ export default class Application {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the application.
|
||||
* @return Promise<void>
|
||||
*/
|
||||
async down() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the application.
|
||||
* @return Promise<void>
|
||||
*/
|
||||
async run() {
|
||||
try {
|
||||
await this.up()
|
||||
@ -49,10 +80,18 @@ export default class Application {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass an error to the top-level error handler.
|
||||
* @param {Error} e
|
||||
*/
|
||||
async app_error(e: Error) {
|
||||
this.rleh.handle(e)
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch the given lifecycle unit.
|
||||
* @param {LifecycleUnit} unit
|
||||
*/
|
||||
protected async start_unit(unit: LifecycleUnit) {
|
||||
try {
|
||||
unit.status = Status.Starting
|
||||
@ -68,18 +107,36 @@ export default class Application {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the root directory of the application.
|
||||
* @type string
|
||||
*/
|
||||
get root() {
|
||||
return path.resolve('.')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the root directory of application class definitions.
|
||||
* @type string
|
||||
*/
|
||||
get app_root() {
|
||||
return path.resolve('./app')
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the given path within the application's root.
|
||||
* @param {...string} parts
|
||||
* @return string
|
||||
*/
|
||||
path(...parts: string[]) {
|
||||
return path.resolve(this.root, ...parts)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the given path within the application's class definition root.
|
||||
* @param {...string} parts
|
||||
* @return string
|
||||
*/
|
||||
app_path(...parts: string[]) {
|
||||
return path.resolve(this.app_root, ...parts)
|
||||
}
|
||||
|
@ -1,28 +1,62 @@
|
||||
import { Status, isStatus } from '../const/status.ts'
|
||||
import { Status } from '../const/status.ts'
|
||||
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 {container} from '../../../di/src/global.ts'
|
||||
import {isInstantiable} from '../../../di/src/type/Instantiable.ts'
|
||||
import AppClass from './AppClass.ts'
|
||||
|
||||
/**
|
||||
* Returns true if the given item is a lifecycle unit.
|
||||
* @param something
|
||||
* @return boolean
|
||||
*/
|
||||
const isLifecycleUnit = (something: any): something is (typeof LifecycleUnit) => {
|
||||
return isInstantiable(something) && something.prototype instanceof LifecycleUnit
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class representing a single unit of the application lifecycle, responsible
|
||||
* for booting and stopping some piece of the application.
|
||||
* @extends AppClass
|
||||
*/
|
||||
export default abstract class LifecycleUnit extends AppClass {
|
||||
/**
|
||||
* The current status of the unit.
|
||||
* @type Status
|
||||
*/
|
||||
private _status = Status.Stopped
|
||||
|
||||
/**
|
||||
* Get the current status of the unit.
|
||||
* @type Status
|
||||
*/
|
||||
public get status() {
|
||||
return this._status
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current status of the unit.
|
||||
* @param {Status} status
|
||||
*/
|
||||
public set status(status) {
|
||||
this._status = status
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called to boot and start the unit when the application is starting.
|
||||
* @return Promise<void>
|
||||
*/
|
||||
public async up(): Promise<void> {};
|
||||
|
||||
/**
|
||||
* Method called to stop the unit when the application is stopping.
|
||||
* @return Promise<void>
|
||||
*/
|
||||
public async down(): Promise<void> {};
|
||||
|
||||
/**
|
||||
* Returns a collection of lifecycle units that this lifecycle unit depends on.
|
||||
* @return Collection<typeof LifecycleUnit>
|
||||
*/
|
||||
public static get_dependencies(): Collection<typeof LifecycleUnit> {
|
||||
if ( isInstantiable(this) ) {
|
||||
const deps = new Collection<typeof LifecycleUnit>()
|
||||
|
@ -2,6 +2,10 @@ import { Service } from '../../../di/src/decorator/Service.ts'
|
||||
|
||||
const service = Service()
|
||||
|
||||
/**
|
||||
* Class decorator that designates a class as a lifecycle unit. Also applies the service decorator.
|
||||
* @constructor
|
||||
*/
|
||||
const Unit = (): ClassDecorator => {
|
||||
return (target) => {
|
||||
return service(target)
|
||||
|
@ -1,6 +1,10 @@
|
||||
import AppClass from '../lifecycle/AppClass.ts'
|
||||
export { Service } from '../../../di/src/decorator/Service.ts'
|
||||
|
||||
/**
|
||||
* Base class for an application service provider.
|
||||
* @extends AppClass
|
||||
*/
|
||||
export class ServiceProvider extends AppClass {
|
||||
|
||||
}
|
||||
|
@ -1,25 +1,49 @@
|
||||
import { LogMessage, LoggingLevel } from './types.ts'
|
||||
import {make} from "../../../../di/src/global.ts";
|
||||
import {Logging} from "./Logging.ts";
|
||||
import {blue, cyan, gray, green, red, yellow} from "../../external/std.ts";
|
||||
import {make} from '../../../../di/src/global.ts'
|
||||
import {Logging} from './Logging.ts'
|
||||
import {blue, cyan, gray, green, red, yellow} from '../../external/std.ts'
|
||||
|
||||
/**
|
||||
* Returns true if the given item is a typeof the Logger class.
|
||||
* @param something
|
||||
* @return boolean
|
||||
*/
|
||||
const isLoggerClass = (something: any): something is (typeof Logger) => {
|
||||
return something.prototype instanceof Logger
|
||||
}
|
||||
|
||||
export { isLoggerClass }
|
||||
|
||||
/**
|
||||
* Base class for an application logger.
|
||||
*/
|
||||
export default abstract class Logger {
|
||||
/**
|
||||
* Write the given message to the log destination.
|
||||
* @param {LogMessage} message
|
||||
* @return Promise<void>
|
||||
*/
|
||||
public abstract async write(message: LogMessage): Promise<void>;
|
||||
|
||||
/**
|
||||
* Register this logger with the logging service.
|
||||
*/
|
||||
public static register() {
|
||||
make(Logging).register_logger(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove this logger from the logging service.
|
||||
*/
|
||||
public static remove() {
|
||||
make(Logging).remove_logger(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the date object to the string output format.
|
||||
* @param {Date} date
|
||||
* @return string
|
||||
*/
|
||||
protected format_date(date: Date): string {
|
||||
const hours = date.getHours()
|
||||
const minutes = date.getMinutes()
|
||||
@ -27,6 +51,11 @@ export default abstract class Logger {
|
||||
return `${date.getFullYear()}-${date.getMonth()+1}-${date.getDate()} ${hours > 9 ? hours : '0' + hours}:${minutes > 9 ? minutes : '0' + minutes}:${seconds > 9 ? seconds : '0' + seconds}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a logging level, get the display string of that level.
|
||||
* @param {LoggingLevel} level
|
||||
* @return string
|
||||
*/
|
||||
protected level_display(level: LoggingLevel): string {
|
||||
switch(level) {
|
||||
case LoggingLevel.Success:
|
||||
|
@ -4,43 +4,99 @@ import {Service} from '../../../../di/src/decorator/Service.ts'
|
||||
import {make} from '../../../../di/src/global.ts'
|
||||
import {isInstantiable} from '../../../../di/src/type/Instantiable.ts'
|
||||
|
||||
/**
|
||||
* Service for managing application logging.
|
||||
*/
|
||||
@Service()
|
||||
class Logging {
|
||||
/**
|
||||
* The current logging level.
|
||||
* @type LoggingLevel
|
||||
*/
|
||||
private _level = LoggingLevel.Warning
|
||||
|
||||
/**
|
||||
* Loggers registered with this service.
|
||||
* @type Array<Logger>
|
||||
*/
|
||||
private _loggers: Logger[] = []
|
||||
|
||||
/**
|
||||
* Get the current logging level.
|
||||
* @type LoggingLevel
|
||||
*/
|
||||
public get level() {
|
||||
return this._level
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the new logging level.
|
||||
* @param {LoggingLevel} level
|
||||
*/
|
||||
public set level(level) {
|
||||
this._level = level
|
||||
}
|
||||
|
||||
/**
|
||||
* Write an output with the success level.
|
||||
* @param output
|
||||
* @param {boolean} [force = false] - if true, the output will be written, regardless of the output level
|
||||
*/
|
||||
public success(output: any, force = false) {
|
||||
this.write_log(LoggingLevel.Success, output, force)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write an output with the error level.
|
||||
* @param output
|
||||
* @param {boolean} [force = false] - if true, the output will be written, regardless of the output level
|
||||
*/
|
||||
public error(output: any, force = false) {
|
||||
this.write_log(LoggingLevel.Error, output, force)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write an output with the warning level.
|
||||
* @param output
|
||||
* @param {boolean} [force = false] - if true, the output will be written, regardless of the output level
|
||||
*/
|
||||
public warn(output: any, force = false) {
|
||||
this.write_log(LoggingLevel.Warning, output, force)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write an output with the info level.
|
||||
* @param output
|
||||
* @param {boolean} [force = false] - if true, the output will be written, regardless of the output level
|
||||
*/
|
||||
public info(output: any, force = false) {
|
||||
this.write_log(LoggingLevel.Info, output, force)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write an output with the debug level.
|
||||
* @param output
|
||||
* @param {boolean} [force = false] - if true, the output will be written, regardless of the output level
|
||||
*/
|
||||
public debug(output: any, force = false) {
|
||||
this.write_log(LoggingLevel.Debug, output, force)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write an output with the verbose level.
|
||||
* @param output
|
||||
* @param {boolean} [force = false] - if true, the output will be written, regardless of the output level
|
||||
*/
|
||||
public verbose(output: any, force = false) {
|
||||
this.write_log(LoggingLevel.Verbose, output, force)
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the output at the given logging level.
|
||||
* @param {LoggingLevel} level
|
||||
* @param output
|
||||
* @param {boolean} [force = false]
|
||||
*/
|
||||
protected write_log(level: LoggingLevel, output: any, force = false) {
|
||||
const message = this.build_message(level, output)
|
||||
if ( this._level >= level || force ) {
|
||||
@ -54,6 +110,12 @@ class Logging {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an output and level, build a log message object.
|
||||
* @param {LoggingLevel} level
|
||||
* @param output
|
||||
* @return LogMessage
|
||||
*/
|
||||
protected build_message(level: LoggingLevel, output: any): LogMessage {
|
||||
return {
|
||||
level,
|
||||
@ -63,6 +125,10 @@ class Logging {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a logger with this class.
|
||||
* @param {typeof Logger} logger_class
|
||||
*/
|
||||
public register_logger(logger_class: typeof Logger) {
|
||||
if ( isInstantiable(logger_class) ) {
|
||||
const logger = make(logger_class)
|
||||
@ -71,10 +137,18 @@ class Logging {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a logger from this class.
|
||||
* @param {typeof Logger} logger_class
|
||||
*/
|
||||
public remove_logger(logger_class: typeof Logger) {
|
||||
this._loggers = this._loggers.filter(x => !(x instanceof logger_class))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the information about the caller of a given context.
|
||||
* @param {number} [level = 5] - how far up in the stacktrace to go
|
||||
*/
|
||||
protected get_caller_info(level = 5): string {
|
||||
let e = new Error
|
||||
if ( !e.stack ) return 'Unknown'
|
||||
|
@ -2,6 +2,10 @@ import AbstractLogger from './Logger.ts'
|
||||
import { LogMessage } from './types.ts'
|
||||
import { gray, cyan } from '../../external/std.ts'
|
||||
|
||||
/**
|
||||
* Logging class that writes to standard output.
|
||||
* @extends AbstractLogger
|
||||
*/
|
||||
export default class StandardLogger extends AbstractLogger {
|
||||
public async write(message: LogMessage): Promise<void> {
|
||||
const prefix = this.level_display(message.level)
|
||||
|
@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Base type for logging levels.
|
||||
*/
|
||||
enum LoggingLevel {
|
||||
Silent = 0,
|
||||
Success = 1,
|
||||
@ -8,6 +11,11 @@ enum LoggingLevel {
|
||||
Verbose = 6,
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given element is a logging level.
|
||||
* @param something
|
||||
* @return boolean
|
||||
*/
|
||||
const isLoggingLevel = (something: any): something is LoggingLevel => {
|
||||
return [
|
||||
LoggingLevel.Silent,
|
||||
@ -20,6 +28,9 @@ const isLoggingLevel = (something: any): something is LoggingLevel => {
|
||||
].includes(something)
|
||||
}
|
||||
|
||||
/**
|
||||
* Base type for a message written to the log.
|
||||
*/
|
||||
interface LogMessage {
|
||||
level: LoggingLevel,
|
||||
date: Date,
|
||||
@ -27,6 +38,11 @@ interface LogMessage {
|
||||
caller_name: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given object is a log message.
|
||||
* @param something
|
||||
* @return boolean
|
||||
*/
|
||||
const isLogMessage = (something: any): something is LogMessage => {
|
||||
return isLoggingLevel(something?.level) && something?.date instanceof Date;
|
||||
}
|
||||
|
@ -2,12 +2,19 @@ import { Service } from '../../../../di/src/decorator/Service.ts'
|
||||
import { Logging } from '../logging/Logging.ts'
|
||||
import {uuid} from '../../external/std.ts'
|
||||
|
||||
/**
|
||||
* Base service with some utility helpers.
|
||||
*/
|
||||
@Service()
|
||||
export default class Utility {
|
||||
constructor(
|
||||
protected logger: Logging
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Make a deep copy of an object.
|
||||
* @param target
|
||||
*/
|
||||
deep_copy<T>(target: T): T {
|
||||
if ( target === null )
|
||||
return target
|
||||
@ -33,6 +40,10 @@ export default class Utility {
|
||||
|
||||
// TODO deep_merge
|
||||
|
||||
/**
|
||||
* Given a string of a value, try to infer the JavaScript type.
|
||||
* @param {string} val
|
||||
*/
|
||||
infer(val: string): any {
|
||||
if ( !val ) return undefined
|
||||
else if ( val.toLowerCase() === 'true' ) return true
|
||||
@ -44,6 +55,10 @@ export default class Utility {
|
||||
else return val
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given value is valid JSON.
|
||||
* @param {string} val
|
||||
*/
|
||||
is_json(val: string): boolean {
|
||||
try {
|
||||
JSON.parse(val)
|
||||
@ -54,6 +69,10 @@ export default class Utility {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a universally-unique ID string.
|
||||
* @return string
|
||||
*/
|
||||
uuid(): string {
|
||||
return uuid()
|
||||
}
|
||||
|
@ -1,29 +1,85 @@
|
||||
/**
|
||||
* Base error used to trigger an unsubscribe action from a subscriber.
|
||||
* @extends Error
|
||||
*/
|
||||
export class UnsubscribeError extends Error {}
|
||||
|
||||
/**
|
||||
* Thrown when a closed observable is pushed to.
|
||||
* @extends Error
|
||||
*/
|
||||
export class CompletedObservableError extends Error {
|
||||
constructor() {
|
||||
super('This observable can no longer be pushed to, as it has been completed.')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type of a basic subscriber function.
|
||||
*/
|
||||
export type SubscriberFunction<T> = (val: T) => any
|
||||
|
||||
/**
|
||||
* Type of a basic subscriber function that handles errors.
|
||||
*/
|
||||
export type SubscriberErrorFunction = (error: Error) => any
|
||||
|
||||
/**
|
||||
* Type of a basic subscriber function that handles completed events.
|
||||
*/
|
||||
export type SubscriberCompleteFunction<T> = (val?: T) => any
|
||||
|
||||
/**
|
||||
* Subscribers that define multiple handler methods.
|
||||
*/
|
||||
export type ComplexSubscriber<T> = {
|
||||
next?: SubscriberFunction<T>,
|
||||
error?: SubscriberErrorFunction,
|
||||
complete?: SubscriberCompleteFunction<T>,
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscription to a behavior subject.
|
||||
*/
|
||||
export type Subscription<T> = SubscriberFunction<T> | ComplexSubscriber<T>
|
||||
|
||||
/**
|
||||
* Object providing helpers for unsubscribing from a subscription.
|
||||
*/
|
||||
export type Unsubscribe = { unsubscribe: () => void }
|
||||
|
||||
/**
|
||||
* A stream-based state class.
|
||||
*/
|
||||
export class BehaviorSubject<T> {
|
||||
/**
|
||||
* Subscribers to this subject.
|
||||
* @type Array<ComplexSubscriber>
|
||||
*/
|
||||
protected subscribers: ComplexSubscriber<T>[] = []
|
||||
|
||||
/**
|
||||
* True if this subject has been marked complete.
|
||||
* @type boolean
|
||||
*/
|
||||
protected _is_complete: boolean = false
|
||||
|
||||
/**
|
||||
* The current value of this subject.
|
||||
*/
|
||||
protected _value?: T
|
||||
|
||||
/**
|
||||
* True if any value has been pushed to this subject.
|
||||
* @type boolean
|
||||
*/
|
||||
protected _has_push: boolean = false
|
||||
|
||||
/**
|
||||
* Register a new subscription to this subject.
|
||||
* @param {Subscription} subscriber
|
||||
* @return Unsubscribe
|
||||
*/
|
||||
public subscribe(subscriber: Subscription<T>): Unsubscribe {
|
||||
if ( typeof subscriber === 'function' ) {
|
||||
this.subscribers.push({ next: subscriber })
|
||||
@ -38,6 +94,10 @@ export class BehaviorSubject<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast this subject to a promise, which resolves on the output of the next value.
|
||||
* @return Promise
|
||||
*/
|
||||
public to_promise(): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { unsubscribe } = this.subscribe({
|
||||
@ -57,6 +117,11 @@ export class BehaviorSubject<T> {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a new value to this subject. The promise resolves when all subscribers have been pushed to.
|
||||
* @param val
|
||||
* @return Promise<void>
|
||||
*/
|
||||
public async next(val: T): Promise<void> {
|
||||
if ( this._is_complete ) throw new CompletedObservableError()
|
||||
this._value = val
|
||||
@ -78,11 +143,23 @@ export class BehaviorSubject<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Push the given array of values to this subject in order.
|
||||
* The promise resolves when all subscribers have been pushed to for all values.
|
||||
* @param {Array} vals
|
||||
* @return Promise<void>
|
||||
*/
|
||||
public async push(vals: T[]): Promise<void> {
|
||||
if ( this._is_complete ) throw new CompletedObservableError()
|
||||
await Promise.all(vals.map(val => this.next(val)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this subject as complete.
|
||||
* The promise resolves when all subscribers have been pushed to.
|
||||
* @param [final_val] - optionally, a final value to set
|
||||
* @return Promise<void>
|
||||
*/
|
||||
public async complete(final_val?: T): Promise<void> {
|
||||
if ( this._is_complete ) throw new CompletedObservableError()
|
||||
if ( typeof final_val === 'undefined' ) final_val = this.value()
|
||||
@ -105,10 +182,17 @@ export class BehaviorSubject<T> {
|
||||
this._is_complete = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current value of this subject.
|
||||
*/
|
||||
public value(): T | undefined {
|
||||
return this._value
|
||||
}
|
||||
|
||||
/**
|
||||
* True if this subject is marked as complete.
|
||||
* @return boolean
|
||||
*/
|
||||
public is_complete(): boolean {
|
||||
return this._is_complete
|
||||
}
|
||||
|
@ -1,6 +1,32 @@
|
||||
|
||||
/**
|
||||
* Abstract interface class for an application cache object.
|
||||
*/
|
||||
export default abstract class Cache {
|
||||
/**
|
||||
* Fetch a value from the cache by its key.
|
||||
* @param {string} key
|
||||
* @return Promise<any|undefined>
|
||||
*/
|
||||
public abstract async fetch(key: string): Promise<any>;
|
||||
|
||||
/**
|
||||
* Store the given value in the cache by key.
|
||||
* @param {string} key
|
||||
* @param {string} value
|
||||
*/
|
||||
public abstract async put(key: string, value: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Check if the cache has the given key.
|
||||
* @param {string} key
|
||||
* @return Promise<boolean>
|
||||
*/
|
||||
public abstract async has(key: string): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Drop the given key from the cache.
|
||||
* @param {string} key
|
||||
*/
|
||||
public abstract async drop(key: string): Promise<void>;
|
||||
}
|
||||
|
@ -6,6 +6,10 @@ import {Collection} from '../collection/Collection.ts'
|
||||
|
||||
// TODO add support for configurable Cache backends
|
||||
|
||||
/**
|
||||
* IoC container factory that produces cache instances.
|
||||
* @extends AbstractFactory
|
||||
*/
|
||||
export default class CacheFactory extends AbstractFactory {
|
||||
constructor() {
|
||||
super({})
|
||||
|
@ -1,12 +1,23 @@
|
||||
import Cache from './Cache.ts'
|
||||
import { Collection } from '../collection/Collection.ts'
|
||||
|
||||
/**
|
||||
* Base interface for an item stored in a memory cache.
|
||||
*/
|
||||
export interface InMemCacheItem {
|
||||
key: string,
|
||||
item: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* A cache implementation stored in memory.
|
||||
* @extends Cache
|
||||
*/
|
||||
export class InMemCache extends Cache {
|
||||
/**
|
||||
* The stored cache items.
|
||||
* @type Collection<InMemCacheItem>
|
||||
*/
|
||||
protected items: Collection<InMemCacheItem> = new Collection<InMemCacheItem>()
|
||||
|
||||
public async fetch(key: string) {
|
||||
|
@ -1,6 +1,13 @@
|
||||
|
||||
/**
|
||||
* Type representing a JSON serializable object.
|
||||
*/
|
||||
export type JSONState = { [key: string]: string | boolean | number | undefined | JSONState | Array<string | boolean | number | undefined | JSONState> }
|
||||
|
||||
/**
|
||||
* Returns true if the given object can be JSON serialized.
|
||||
* @param what
|
||||
* @return boolean
|
||||
*/
|
||||
export function isJSONState(what: any): what is JSONState {
|
||||
try {
|
||||
JSON.stringify(what)
|
||||
@ -10,7 +17,20 @@ export function isJSONState(what: any): what is JSONState {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base interface for a class that can be rehydrated and restored.
|
||||
*/
|
||||
export interface Rehydratable {
|
||||
/**
|
||||
* Dehydrate this class' state and get it.
|
||||
* @return Promise<JSONState>
|
||||
*/
|
||||
dehydrate(): Promise<JSONState>
|
||||
|
||||
/**
|
||||
* Rehydrate a state into this class.
|
||||
* @param {JSONState} state
|
||||
* @return void|Promise<void>
|
||||
*/
|
||||
rehydrate(state: JSONState): void | Promise<void>
|
||||
}
|
||||
|
@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Base type for an API response format.
|
||||
*/
|
||||
export interface APIResponse {
|
||||
success: boolean,
|
||||
message?: string,
|
||||
@ -9,6 +12,11 @@ export interface APIResponse {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a mesage as a successful API response.
|
||||
* @param {string} message
|
||||
* @return APIResponse
|
||||
*/
|
||||
export function message(message: string): APIResponse {
|
||||
return {
|
||||
success: true,
|
||||
@ -16,6 +24,11 @@ export function message(message: string): APIResponse {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a single record as a successful API response.
|
||||
* @param record
|
||||
* @return APIResponse
|
||||
*/
|
||||
export function one(record: any): APIResponse {
|
||||
return {
|
||||
success: true,
|
||||
@ -23,6 +36,11 @@ export function one(record: any): APIResponse {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats an array of records as a successful API response.
|
||||
* @param {array} records
|
||||
* @return APIResponse
|
||||
*/
|
||||
export function many(records: any[]): APIResponse {
|
||||
return {
|
||||
success: true,
|
||||
@ -33,6 +51,11 @@ export function many(records: any[]): APIResponse {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats an error message or Error instance as an API response.
|
||||
* @param {string|Error} error
|
||||
* @return APIResponse
|
||||
*/
|
||||
export function error(error: string | Error): APIResponse {
|
||||
if ( typeof error === 'string' ) {
|
||||
return {
|
||||
|
@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Apply the given mixin classes to the given constructor.
|
||||
* @param derivedCtor
|
||||
* @param {array} baseCtors
|
||||
*/
|
||||
export function applyMixins(derivedCtor: any, baseCtors: any[]) {
|
||||
baseCtors.forEach(baseCtor => {
|
||||
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
|
||||
@ -8,4 +13,7 @@ export function applyMixins(derivedCtor: any, baseCtors: any[]) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Base type for a constructor function.
|
||||
*/
|
||||
export type Constructor<T = {}> = new (...args: any[]) => T
|
||||
|
@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Base interface representing a timeout subscriber.
|
||||
*/
|
||||
export interface TimeoutSubscriber<T> {
|
||||
on_time: (handler: (arg: T) => any) => TimeoutSubscriber<T>,
|
||||
late: (handler: (arg: T) => any) => TimeoutSubscriber<T>,
|
||||
@ -5,6 +8,11 @@ export interface TimeoutSubscriber<T> {
|
||||
run: () => Promise<T>,
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to a promise with a timeout.
|
||||
* @param {number} timeout - timeout in milliseconds
|
||||
* @param {Promise} promise - the promise to subscribe to
|
||||
*/
|
||||
export function withTimeout<T>(timeout: number, promise: Promise<T>) {
|
||||
let on_time_handler: (arg: T) => any = (arg) => {}
|
||||
let late_handler: (arg: T) => any = (arg) => {}
|
||||
|
@ -1,12 +1,20 @@
|
||||
import {Service} from '../../../di/src/decorator/Service.ts'
|
||||
import {Canonical} from './Canonical.ts'
|
||||
|
||||
/**
|
||||
* 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.`)
|
||||
@ -14,15 +22,32 @@ export class NoSuchCanonicalResolverKeyError extends Error {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Service used to access various canonical resources.
|
||||
*/
|
||||
@Service()
|
||||
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
|
||||
*/
|
||||
register_canonical(unit: Canonical<any>) {
|
||||
const key = unit.canonical_items
|
||||
if ( this.resources[key] ) throw new DuplicateResolverKeyError(key)
|
||||
|
@ -3,24 +3,60 @@ import {fs, path} from '../external/std.ts'
|
||||
import {Canon} from './Canon.ts'
|
||||
import {Logging} from '../service/logging/Logging.ts'
|
||||
|
||||
/**
|
||||
* Base type for a canonical definition.
|
||||
*/
|
||||
export interface CanonicalDefinition {
|
||||
canonical_name: string,
|
||||
original_name: string,
|
||||
imported: any,
|
||||
}
|
||||
|
||||
/**
|
||||
* Base type for a canonical name reference.
|
||||
*/
|
||||
export interface CanonicalReference {
|
||||
resource?: string,
|
||||
item: string,
|
||||
particular?: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for all canonical units. Provides helpers for reading and standardizing
|
||||
* the names of classes defined in the filesystem structure.
|
||||
* @extends LifecycleUnit
|
||||
*/
|
||||
export class Canonical<T> extends LifecycleUnit {
|
||||
/**
|
||||
* The base path directory where the canonical definitions reside.
|
||||
* @type string
|
||||
*/
|
||||
protected base_path: string = '.'
|
||||
|
||||
/**
|
||||
* The file suffix of files in the base path that should be loaded.
|
||||
* @type string
|
||||
*/
|
||||
protected suffix: string = '.ts'
|
||||
|
||||
/**
|
||||
* The singular, programmatic name of one of these canonical items.
|
||||
* @example middleware
|
||||
* @type string
|
||||
*/
|
||||
protected canonical_item: string = ''
|
||||
|
||||
/**
|
||||
* Object mapping canonical names to loaded file references.
|
||||
* @type object
|
||||
*/
|
||||
protected _items: { [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 rsc_parts = reference.split('::')
|
||||
const resource = rsc_parts.length > 1 ? rsc_parts[0] + 's' : undefined
|
||||
@ -34,14 +70,26 @@ export class Canonical<T> extends LifecycleUnit {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of all canonical reference names.
|
||||
* @return Array<string>
|
||||
*/
|
||||
public all(): string[] {
|
||||
return Object.keys(this._items)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fully-qualified path to the base directory for this unit.
|
||||
* @type string
|
||||
*/
|
||||
public get path(): string {
|
||||
return path.resolve(this.base_path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the plural, programmatic name of the canonical items provide by this unit.
|
||||
* @type string
|
||||
*/
|
||||
public get canonical_items() {
|
||||
return `${this.canonical_item}s`
|
||||
}
|
||||
@ -60,10 +108,22 @@ export class Canonical<T> extends LifecycleUnit {
|
||||
this.make(Canon).register_canonical(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a single canonical definition loaded from a file in the base path,
|
||||
* initialize the item and return the result that should be mapped to the reference name.
|
||||
* @param {CanonicalDefinition} definition
|
||||
* @return Promise<any>
|
||||
*/
|
||||
public async init_canonical_item(definition: CanonicalDefinition): Promise<T> {
|
||||
return definition.imported.default
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a file path, build the canonical definition represented by that path.
|
||||
* @param {string} file_path
|
||||
* @private
|
||||
* @return Promise<CanonicalDefinition>
|
||||
*/
|
||||
private async _get_canonical_definition(file_path: string): Promise<CanonicalDefinition> {
|
||||
const original_name = file_path.replace(this.path, '').substr(1)
|
||||
const path_regex = new RegExp(path.SEP, 'g')
|
||||
@ -75,6 +135,11 @@ export class Canonical<T> extends LifecycleUnit {
|
||||
return { canonical_name, original_name, imported }
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a canonical reference string, get the corresponding item, if it exists.
|
||||
* @param {string} key
|
||||
* @return any | undefined
|
||||
*/
|
||||
public get(key: string): T | undefined {
|
||||
return this._items[key]
|
||||
}
|
||||
|
@ -1,6 +1,10 @@
|
||||
import { Unit } from '../lifecycle/decorators.ts'
|
||||
import {RecursiveCanonical} from './RecursiveCanonical.ts'
|
||||
|
||||
/**
|
||||
* Canonical unit which loads the config files into memory.
|
||||
* @extends RecursiveCanonical
|
||||
*/
|
||||
@Unit()
|
||||
export default class Config extends RecursiveCanonical {
|
||||
protected base_path = './app/configs'
|
||||
|
@ -3,6 +3,10 @@ import { CanonicalDefinition } from './Canonical.ts'
|
||||
import Controller from '../http/Controller.ts'
|
||||
import { Unit } from '../lifecycle/decorators.ts'
|
||||
|
||||
/**
|
||||
* Canonical method which loads controllers, and returns an instance of the controller.
|
||||
* @extends InstantiableCanonical
|
||||
*/
|
||||
@Unit()
|
||||
export default class Controllers extends InstantiableCanonical<Controller> {
|
||||
protected base_path = './app/http/controllers'
|
||||
|
@ -20,6 +20,10 @@ import PersistSession from '../http/kernel/module/PersistSession.ts'
|
||||
import MountActivatedRoute from '../http/kernel/module/MountActivatedRoute.ts'
|
||||
import ApplyRouteHandlers from '../http/kernel/module/ApplyRouteHandlers.ts'
|
||||
|
||||
/**
|
||||
* Lifecycle unit which bootstraps the HTTP kernel modules, and sets the session provider.
|
||||
* @extends LifecycleUnit
|
||||
*/
|
||||
@Unit()
|
||||
export default class HttpKernel extends LifecycleUnit {
|
||||
constructor(
|
||||
@ -37,6 +41,9 @@ export default class HttpKernel extends LifecycleUnit {
|
||||
this.register_modules()
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the session provider from the config and register the appropriate factories.
|
||||
*/
|
||||
protected determine_session_provider() {
|
||||
const driver = this.config.get('server.session.driver')
|
||||
|
||||
@ -63,6 +70,9 @@ export default class HttpKernel extends LifecycleUnit {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the default HTTP kernel modules with the kernel.
|
||||
*/
|
||||
protected register_modules() {
|
||||
PrepareRequest.register(this.kernel)
|
||||
SetSessionCookie.register(this.kernel)
|
||||
|
@ -9,6 +9,10 @@ import {http} from '../http/response/helpers.ts'
|
||||
import {HTTPStatus} from '../const/http.ts'
|
||||
import Config from './Config.ts'
|
||||
|
||||
/**
|
||||
* Lifecycle unit which starts the HTTP server.
|
||||
* @extends LifecycleUnit
|
||||
*/
|
||||
@Unit()
|
||||
export default class HttpServer extends LifecycleUnit {
|
||||
protected _server: any // TODO replace with more specific type
|
||||
|
@ -1,12 +1,21 @@
|
||||
import {Canonical, CanonicalDefinition} from './Canonical.ts'
|
||||
import Instantiable, {isInstantiable} from '../../../di/src/type/Instantiable.ts'
|
||||
|
||||
/**
|
||||
* Error thrown when the item returned from a canonical definition file is not the expected item.
|
||||
* @extends Error
|
||||
*/
|
||||
export class InvalidCanonicalExportError extends Error {
|
||||
constructor(name: string) {
|
||||
super(`Unable to import canonical item from "${name}". The default export of this file is invalid.`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for Canonical units which return instantiated versions of the classes
|
||||
* defined in those files. The files should export default clases which are Instantiable.
|
||||
* @extends Canonical
|
||||
*/
|
||||
export class InstantiableCanonical<T> extends Canonical<Instantiable<T>> {
|
||||
public async init_canonical_item(def: CanonicalDefinition): Promise<Instantiable<T>> {
|
||||
if ( isInstantiable(def.imported.default) ) {
|
||||
|
@ -3,6 +3,10 @@ import { CanonicalDefinition } from './Canonical.ts'
|
||||
import Middleware from '../http/Middleware.ts'
|
||||
import { Unit } from '../lifecycle/decorators.ts'
|
||||
|
||||
/**
|
||||
* Canonical unit which loads and instantiates application middleware.
|
||||
* @extends InstantiableCanonical
|
||||
*/
|
||||
@Unit()
|
||||
export default class Middlewares extends InstantiableCanonical<Middleware> {
|
||||
protected base_path = './app/http/middleware'
|
||||
|
@ -1,5 +1,8 @@
|
||||
import {Canonical} from './Canonical.ts'
|
||||
|
||||
/**
|
||||
* Special canonical unit which deep-resolves values recursively.
|
||||
*/
|
||||
export class RecursiveCanonical extends Canonical<any> {
|
||||
public get(key: string, fallback?: any): any | undefined {
|
||||
const parts = key.split('.')
|
||||
|
@ -2,6 +2,10 @@ import {Canonical, CanonicalDefinition} from './Canonical.ts'
|
||||
import {isRouterDefinition, RouterDefinition} from '../http/type/RouterDefinition.ts'
|
||||
import {Unit} from '../lifecycle/decorators.ts'
|
||||
|
||||
/**
|
||||
* Canonical unit which loads router definitions.
|
||||
* @extends Canonical
|
||||
*/
|
||||
@Unit()
|
||||
export default class Routes extends Canonical<RouterDefinition> {
|
||||
protected base_path = './app/http/routes'
|
||||
|
@ -16,8 +16,19 @@ import {isBindable} from '../lifecycle/AppClass.ts'
|
||||
import {DeepmatchRoute} from "../http/routing/DeepmatchRoute.ts";
|
||||
import {RegExRoute} from "../http/routing/RegExRoute.ts";
|
||||
|
||||
/**
|
||||
* Base type defining a single route handler.
|
||||
*/
|
||||
export type RouteHandler = (request: Request) => Request | Promise<Request> | ResponseFactory | Promise<ResponseFactory> | void | Promise<void>
|
||||
|
||||
/**
|
||||
* Base type for a collection of route handlers.
|
||||
*/
|
||||
export type RouteHandlers = RouteHandler[]
|
||||
|
||||
/**
|
||||
* Base type for a router definition.
|
||||
*/
|
||||
export interface RouteDefinition {
|
||||
get?: RouteHandlers,
|
||||
post?: RouteHandlers,
|
||||
@ -30,6 +41,11 @@ export interface RouteDefinition {
|
||||
trace?: RouteHandlers,
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given object is a valid route handler.
|
||||
* @param what
|
||||
* @return boolean
|
||||
*/
|
||||
export function isRouteHandler(what: any): what is RouteHandler {
|
||||
return (
|
||||
typeof what === 'function'
|
||||
@ -39,9 +55,22 @@ export function isRouteHandler(what: any): what is RouteHandler {
|
||||
|
||||
const verbs = ['get', 'post', 'patch', 'delete', 'head', 'put', 'connect', 'options', 'trace']
|
||||
|
||||
/**
|
||||
* Lifecycle unit which processes the loaded routes and builds Route instances from them.
|
||||
* @extends LifecycleUnit
|
||||
*/
|
||||
@Unit()
|
||||
export default class Routing extends LifecycleUnit {
|
||||
/**
|
||||
* Mapping of route definition strings to route definitions.
|
||||
* @type object
|
||||
*/
|
||||
protected definitions: { [key: string]: RouteDefinition } = {}
|
||||
|
||||
/**
|
||||
* Collection of Route instances
|
||||
* @type Collection<Route>
|
||||
*/
|
||||
protected instances: Collection<Route> = new Collection<Route>()
|
||||
|
||||
constructor(
|
||||
@ -82,6 +111,11 @@ export default class Routing extends LifecycleUnit {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a group of canonical-string handlers, build an array of route handlers.
|
||||
* @param {Array<string>} group
|
||||
* @return RouteHandlers
|
||||
*/
|
||||
public build_handler(group: string[]): RouteHandlers {
|
||||
const handlers: RouteHandlers = []
|
||||
for ( const item of group ) {
|
||||
@ -124,6 +158,11 @@ export default class Routing extends LifecycleUnit {
|
||||
return handlers
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a set of route parts, resolve them to a string.
|
||||
* @param {Array<string>} parts
|
||||
* @return string
|
||||
*/
|
||||
public resolve(parts: string[]): string {
|
||||
const cleaned = parts.map(part => {
|
||||
if ( part.startsWith('/') ) part = part.substr(1)
|
||||
@ -137,6 +176,12 @@ export default class Routing extends LifecycleUnit {
|
||||
return `/${joined}`.toLowerCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a base and a key, return a new Route instance.
|
||||
* @param {string} base
|
||||
* @param {string} key
|
||||
* @return Route
|
||||
*/
|
||||
public build_route(base: string, key: string): Route {
|
||||
if ( key.startsWith('rex ') ) {
|
||||
return new RegExRoute(base.split(key)[0], key)
|
||||
@ -149,10 +194,21 @@ export default class Routing extends LifecycleUnit {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the route instance given an incoming route string, if one exists.
|
||||
* @param {string} incoming
|
||||
* @return Route | undefined
|
||||
*/
|
||||
public match(incoming: string): Route | undefined {
|
||||
return this.instances.firstWhere((route: Route) => route.match(incoming))
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an incoming route and HTTP method, build an activated route if a matching route is found.
|
||||
* @param {string} incoming
|
||||
* @param {string} method
|
||||
* @return ActivatedRoute | undefined
|
||||
*/
|
||||
public build(incoming: string, method: string): ActivatedRoute | undefined {
|
||||
const route: Route | undefined = this.match(incoming)
|
||||
|
||||
|
@ -9,9 +9,13 @@ import 'https://deno.land/x/dotenv/load.ts'
|
||||
import { Container } from '../../../di/src/Container.ts'
|
||||
import { Inject } from '../../../di/src/decorator/Injection.ts'
|
||||
import CacheFactory from '../support/CacheFactory.ts'
|
||||
import SessionFactory from '../http/session/SessionFactory.ts'
|
||||
import SessionManagerFactory from '../http/session/SessionManagerFactory.ts'
|
||||
|
||||
/**
|
||||
* Simple helper for loading ENV values with fallback.
|
||||
* @param {string} name - the environment variable name
|
||||
* @param fallback
|
||||
* @return any
|
||||
*/
|
||||
const env = (name: string, fallback?: any) => {
|
||||
const scaffolding = make(Scaffolding)
|
||||
return scaffolding.env(name) ?? fallback
|
||||
@ -19,6 +23,10 @@ const env = (name: string, fallback?: any) => {
|
||||
|
||||
export { env }
|
||||
|
||||
/**
|
||||
* Unit service responsible for getting basic essential scaffolding necessary for Daton.
|
||||
* @extends LifecycleUnit
|
||||
*/
|
||||
@Unit()
|
||||
export default class Scaffolding extends LifecycleUnit {
|
||||
constructor(
|
||||
@ -27,6 +35,11 @@ export default class Scaffolding extends LifecycleUnit {
|
||||
@Inject('injector') protected injector: Container,
|
||||
) { super() }
|
||||
|
||||
/**
|
||||
* Helper method for fetching environment variables.
|
||||
* @param {string} name
|
||||
* @return any
|
||||
*/
|
||||
public env(name: string) {
|
||||
return this.utility.infer(Deno.env.get(name) ?? '')
|
||||
}
|
||||
@ -36,6 +49,9 @@ export default class Scaffolding extends LifecycleUnit {
|
||||
this.register_factories()
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap the logging service, and set the appropriate logging level.
|
||||
*/
|
||||
public setup_logging() {
|
||||
StandardLogger.register()
|
||||
|
||||
@ -53,6 +69,9 @@ export default class Scaffolding extends LifecycleUnit {
|
||||
this.logger.info('Logging initialized.', true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the necessary core factories with the IoC container.
|
||||
*/
|
||||
public register_factories() {
|
||||
this.logger.verbose('Adding the cache production factory to the container...')
|
||||
this.injector.register_factory(new CacheFactory())
|
||||
|
@ -2,6 +2,9 @@ import {InstantiableCanonical} from './InstantiableCanonical.ts'
|
||||
import {ServiceProvider} from '../service/ServiceProvider.ts'
|
||||
import {CanonicalDefinition} from './Canonical.ts'
|
||||
|
||||
/**
|
||||
* Canonical unit which loads user-defined services.
|
||||
*/
|
||||
export default class Services extends InstantiableCanonical<ServiceProvider> {
|
||||
protected base_path = './app/services'
|
||||
protected canonical_item = 'service'
|
||||
|
@ -2,6 +2,10 @@ import {Canonical, CanonicalDefinition} from './Canonical.ts'
|
||||
import {InvalidCanonicalExportError} from './InstantiableCanonical.ts'
|
||||
import {isStaticClass, StaticClass} from '../../../di/src/type/StaticClass.ts'
|
||||
|
||||
/**
|
||||
* Base canonical unit which loads static classes from their canonical files.
|
||||
* @extends Canonical
|
||||
*/
|
||||
export class StaticCanonical<T, T2> extends Canonical<StaticClass<T, T2>> {
|
||||
public async init_canonical_item(def: CanonicalDefinition): Promise<StaticClass<T, T2>> {
|
||||
if ( isStaticClass(def.imported.default) ) {
|
||||
|
@ -4,8 +4,16 @@ import {Handlebars} from '../external/http.ts'
|
||||
import {Logging} from '../service/logging/Logging.ts'
|
||||
import {fs} from '../external/std.ts'
|
||||
|
||||
/**
|
||||
* Lifecycle unit which sets up and provides basic view engine services.
|
||||
* @extends LifecycleUnit
|
||||
*/
|
||||
@Unit()
|
||||
export default class ViewEngine extends LifecycleUnit {
|
||||
/**
|
||||
* The Handlebars instance.
|
||||
* @type Handlebars
|
||||
*/
|
||||
protected _handlebars!: Handlebars
|
||||
|
||||
// TODO include basic app info in view data
|
||||
@ -41,15 +49,31 @@ export default class ViewEngine extends LifecycleUnit {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The handlebars instance.
|
||||
* @type Handlebars
|
||||
*/
|
||||
get handlebars(): Handlebars {
|
||||
return this._handlebars
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a view with the given name, using the specified arguments and layout.
|
||||
* @param {string} view
|
||||
* @param [args]
|
||||
* @param {string} [layout]
|
||||
* @return Promise<string>
|
||||
*/
|
||||
async render(view: string, args?: any, layout?: string): Promise<string> {
|
||||
this.logger.debug(`Rendering view: ${view}`)
|
||||
return this.handlebars.renderView(view, args, layout)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a partial view with the given name, using the specified arguments.
|
||||
* @param {string} view
|
||||
* @param [args]
|
||||
*/
|
||||
async partial(view: string, args?: any) {
|
||||
const parts = `${view}.hbs`.split(':')
|
||||
const resolved = this.app.app_path('http', 'views', ...parts)
|
||||
|
@ -3,6 +3,10 @@ import Config from '../../lib/src/unit/Config.ts'
|
||||
import {Unit} from '../../lib/src/lifecycle/decorators.ts'
|
||||
import Database from './service/Database.ts'
|
||||
|
||||
/**
|
||||
* Lifecycle unit which loads and creates database connections from the database config files.
|
||||
* @extends LifecycleUnit
|
||||
*/
|
||||
@Unit()
|
||||
export class DatabaseUnit extends LifecycleUnit {
|
||||
constructor(
|
||||
|
@ -3,6 +3,10 @@ import {Model} from './model/Model.ts'
|
||||
import {Unit} from '../../lib/src/lifecycle/decorators.ts'
|
||||
import {StaticCanonical} from '../../lib/src/unit/StaticCanonical.ts'
|
||||
|
||||
/**
|
||||
* Canonical unit which loads ORM models from their directory.
|
||||
* @extends StaticCanonical
|
||||
*/
|
||||
@Unit()
|
||||
export default class ModelsUnit extends StaticCanonical<Model<any>, typeof Model> {
|
||||
protected base_path = './app/models'
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {escape, EscapedValue, FieldSet, QuerySource} from './types.ts'
|
||||
import {EscapedValue, FieldSet, QuerySource} from './types.ts'
|
||||
import {Select} from './type/Select.ts'
|
||||
import RawValue from './RawValue.ts'
|
||||
import {Statement} from './Statement.ts'
|
||||
@ -7,55 +7,111 @@ import {Insert} from './type/Insert.ts'
|
||||
import {Delete} from './type/Delete.ts'
|
||||
import {Truncate} from './type/Truncate.ts'
|
||||
|
||||
/**
|
||||
* Wrap a string so it gets included in the query unescaped.
|
||||
* @param {string} value
|
||||
* @return RawValue
|
||||
*/
|
||||
export function raw(value: string) {
|
||||
return new RawValue(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when an interpolated statement has an incorrect number of arguments.
|
||||
* @extends Error
|
||||
*/
|
||||
export class IncorrectInterpolationError extends Error {
|
||||
constructor(expected: number, received: number) {
|
||||
super(`Unable to interpolate arguments into query. Expected ${expected} argument${expected === 1 ? '' : 's'}, but received ${received}.`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base query builder class used to start various types of queries.
|
||||
*/
|
||||
export class Builder<T> {
|
||||
// create table, alter table, drop table, select
|
||||
|
||||
/**
|
||||
* Get a new SELECT statement.
|
||||
* @param {...FieldSet} fields
|
||||
* @return Select
|
||||
*/
|
||||
public select(...fields: FieldSet[]): Select<T> {
|
||||
fields = fields.flat()
|
||||
const select = new Select<T>()
|
||||
return select.fields(...fields)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new UPDATE statement.
|
||||
* @param {QuerySource} [target]
|
||||
* @param {string} [alias]
|
||||
* @return Update
|
||||
*/
|
||||
public update(target?: QuerySource, alias?: string): Update<T> {
|
||||
const update = new Update<T>()
|
||||
if ( target ) update.to(target, alias)
|
||||
return update
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new DELETE statement.
|
||||
* @param {QuerySource} [target]
|
||||
* @param {string} [alias]
|
||||
* @return Delete
|
||||
*/
|
||||
public delete(target?: QuerySource, alias?: string): Delete<T> {
|
||||
const del = new Delete<T>()
|
||||
if ( target ) del.from(target, alias)
|
||||
return del
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new INSERT statement.
|
||||
* @param {QuerySource} [target]
|
||||
* @param {string} [alias]
|
||||
* @return Insert
|
||||
*/
|
||||
public insert(target?: QuerySource, alias?: string): Insert<T> {
|
||||
const insert = new Insert<T>()
|
||||
if ( target ) insert.into(target, alias)
|
||||
return insert
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new raw SQL statement.
|
||||
* @param {string} statement
|
||||
* @param {...EscapedValue} interpolations
|
||||
* @return Statement
|
||||
*/
|
||||
public statement(statement: string, ...interpolations: EscapedValue[]): Statement<T> {
|
||||
return new Statement<T>(statement, interpolations)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new TRUNCATE statement.
|
||||
* @param {QuerySource} [target]
|
||||
* @param {string} [alias]
|
||||
* @return Truncate
|
||||
*/
|
||||
public truncate(target?: QuerySource, alias?: string): Truncate<T> {
|
||||
return new Truncate<T>(target, alias)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a string so it gets included in the query unescaped.
|
||||
* @param {string} value
|
||||
* @return RawValue
|
||||
*/
|
||||
public static raw(value: string) {
|
||||
return new RawValue(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the 'DEFAULT' operator, raw.
|
||||
* @return RawValue
|
||||
*/
|
||||
public static default() {
|
||||
return this.raw('DEFAULT')
|
||||
}
|
||||
|
@ -1,5 +1,14 @@
|
||||
/**
|
||||
* Query builder helper that represents a string that should be directly interpolated
|
||||
* into the SQL of a given query, without being escaped.
|
||||
*/
|
||||
export default class RawValue {
|
||||
constructor(
|
||||
/**
|
||||
* The value to be interpolated.
|
||||
* @type string
|
||||
* @readonly
|
||||
*/
|
||||
public readonly value: string
|
||||
) {}
|
||||
}
|
||||
|
@ -1,5 +1,15 @@
|
||||
import {WhereBuilder} from './type/WhereBuilder.ts'
|
||||
|
||||
/**
|
||||
* Abstract base class for query builder scopes.
|
||||
* @abstract
|
||||
*/
|
||||
export abstract class Scope {
|
||||
/**
|
||||
* Applies this scope to the incoming query.
|
||||
* @param {WhereBuilder} query
|
||||
* @return WhereBuilder
|
||||
* @abstract
|
||||
*/
|
||||
abstract apply(query: WhereBuilder): WhereBuilder
|
||||
}
|
||||
|
@ -2,9 +2,21 @@ import {EscapedValue, escape} from './types.ts'
|
||||
import {IncorrectInterpolationError} from './Builder.ts'
|
||||
import ConnectionExecutable from './type/ConnectionExecutable.ts'
|
||||
|
||||
/**
|
||||
* Query builder base class for a raw SQL statement.
|
||||
* @extends ConnectionExecutable
|
||||
*/
|
||||
export class Statement<T> extends ConnectionExecutable<T> {
|
||||
constructor(
|
||||
/**
|
||||
* The statement to be executed.
|
||||
* @type string
|
||||
*/
|
||||
public statement: string,
|
||||
/**
|
||||
* The variables to be interpolated into the statement.
|
||||
* @type Array<EscapedValue>
|
||||
*/
|
||||
public interpolations: EscapedValue[]
|
||||
) {
|
||||
super()
|
||||
|
@ -1,10 +1,23 @@
|
||||
import {Scope} from '../Scope.ts'
|
||||
import {WhereBuilder} from '../type/WhereBuilder.ts'
|
||||
|
||||
/**
|
||||
* Base type of functions which provide a query scope.
|
||||
*/
|
||||
export type ScopeFunction = (query: WhereBuilder) => WhereBuilder
|
||||
|
||||
/**
|
||||
* Query scope class which builds its clauses by calling an external function.
|
||||
* @extends Scope
|
||||
*/
|
||||
export class FunctionScope extends Scope {
|
||||
constructor(protected _fn: ScopeFunction) {
|
||||
constructor(
|
||||
/**
|
||||
* The scope function used to scope the query.
|
||||
* @type ScopeFunction
|
||||
*/
|
||||
protected _fn: ScopeFunction
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
|
@ -9,13 +9,31 @@ import ResultOperator from './result/ResultOperator.ts'
|
||||
import {collect, Collection} from '../../../../lib/src/collection/Collection.ts'
|
||||
import NoTargetOperatorError from '../../error/NoTargetOperatorError.ts'
|
||||
|
||||
/**
|
||||
* Base class for a query that can be executed in a database connection.
|
||||
* @abstract
|
||||
*/
|
||||
export default abstract class ConnectionExecutable<T> {
|
||||
/**
|
||||
* Render the query to raw SQL, starting with the base indentation level.
|
||||
* @param {number} level
|
||||
* @return string
|
||||
*/
|
||||
abstract sql(level: number): string
|
||||
|
||||
/**
|
||||
* Cast the query to an SQL statement which counts the incoming rows.
|
||||
* @return string
|
||||
*/
|
||||
to_count(): string {
|
||||
return `SELECT COUNT(*) AS to_count FROM (${this.sql(0)}) AS target_query`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the result row for this query at index i.
|
||||
* @param {number} i
|
||||
* @return Promise<any>
|
||||
*/
|
||||
async get_row(i: number): Promise<T | undefined> {
|
||||
if ( !(this.__target_connection instanceof Connection) ) {
|
||||
throw new Error('Unable to execute database item: no target connection.')
|
||||
@ -34,6 +52,12 @@ export default abstract class ConnectionExecutable<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a range of resultant rows for this query between the start and end indices.
|
||||
* @param {string} start
|
||||
* @param {string} end
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async get_range(start: number, end: number): Promise<Collection<T>> {
|
||||
if ( !(this.__target_connection instanceof Connection) ) {
|
||||
throw new Error('Unable to execute database item: no target connection.')
|
||||
@ -52,27 +76,59 @@ export default abstract class ConnectionExecutable<T> {
|
||||
return inflated
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an iterator for this result set.
|
||||
* @return ResultIterable
|
||||
*/
|
||||
iterator(): ResultIterable<T> {
|
||||
return new ResultIterable<T>(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the results as an async collection, with the processing chunk size.
|
||||
* @param {number} chunk_size
|
||||
* @return ResultCollection
|
||||
*/
|
||||
results(chunk_size = 1000) {
|
||||
return new ResultCollection<T>(this.iterator(), chunk_size)
|
||||
}
|
||||
|
||||
/**
|
||||
* The database connection to execute the statement in.
|
||||
* @type Connection
|
||||
*/
|
||||
__target_connection?: Connection
|
||||
|
||||
/**
|
||||
* The result operator to use to process the incoming rows.
|
||||
* @type ResultOperator
|
||||
*/
|
||||
__target_operator?: ResultOperator<T>
|
||||
|
||||
/**
|
||||
* Set the target connection.
|
||||
* @param {string|Connection} connection - the connection or connection name
|
||||
* @return ConnectionExecutable
|
||||
*/
|
||||
target_connection(connection: string | Connection) {
|
||||
this.__target_connection = typeof connection === 'string' ? make(Database).connection(connection) : connection
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the target operator.
|
||||
* @param {ResultOperator} operator
|
||||
* @return ConnectionExecutable
|
||||
*/
|
||||
target_operator(operator: ResultOperator<T>) {
|
||||
this.__target_operator = operator
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query and get back the raw result.
|
||||
* @return Promise<QueryResult>
|
||||
*/
|
||||
async execute(): Promise<QueryResult> {
|
||||
if ( !(this.__target_connection instanceof Connection) ) {
|
||||
throw new Error('Unable to execute database item: no target connection.')
|
||||
@ -81,6 +137,10 @@ export default abstract class ConnectionExecutable<T> {
|
||||
return this.execute_in_connection(this.__target_connection)
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of returned rows.
|
||||
* @return Promise<number>
|
||||
*/
|
||||
async count(): Promise<number> {
|
||||
if ( !(this.__target_connection instanceof Connection) ) {
|
||||
throw new Error('Unable to execute database item: no target connection.')
|
||||
@ -92,10 +152,19 @@ export default abstract class ConnectionExecutable<T> {
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the number of rows returned is greater than 0.
|
||||
* @return Promise<boolean>
|
||||
*/
|
||||
async exists(): Promise<boolean> {
|
||||
return (await this.count()) > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query in the given connection and return the raw result.
|
||||
* @param {string|Connection} connection - the connection or connection name
|
||||
* @return Promise<QueryResult>
|
||||
*/
|
||||
async execute_in_connection(connection: string | Connection): Promise<QueryResult> {
|
||||
const conn = typeof connection === 'string' ? make(Database).connection(connection) : connection
|
||||
|
||||
|
@ -4,7 +4,17 @@ import {Connection} from '../../db/Connection.ts'
|
||||
import {Collection} from '../../../../lib/src/collection/Collection.ts'
|
||||
import NoTargetOperatorError from '../../error/NoTargetOperatorError.ts'
|
||||
|
||||
/**
|
||||
* Variant of the ConnectionExecutable used to build queries that mutate data. This
|
||||
* structure overrides methods to ensure that the query is run only once.
|
||||
* @extends ConnectionExecutable
|
||||
* @abstract
|
||||
*/
|
||||
export default abstract class ConnectionMutable<T> extends ConnectionExecutable<T> {
|
||||
/**
|
||||
* The cached execution result.
|
||||
* @type QueryResult
|
||||
*/
|
||||
__execution_result?: QueryResult
|
||||
|
||||
async get_row(i: number): Promise<T | undefined> {
|
||||
@ -28,6 +38,11 @@ export default abstract class ConnectionMutable<T> extends ConnectionExecutable<
|
||||
return result.row_count
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query result. Executes the query if it hasn't already. If it has,
|
||||
* return the cached query result.
|
||||
* @return Promise<QueryResult>
|
||||
*/
|
||||
async get_execution_result(): Promise<QueryResult> {
|
||||
if ( this.__execution_result ) return this.__execution_result
|
||||
else return this.execute()
|
||||
|
@ -6,11 +6,41 @@ import {TableRefBuilder} from './TableRefBuilder.ts'
|
||||
import {MalformedSQLGrammarError} from './Select.ts'
|
||||
import {Scope} from '../Scope.ts'
|
||||
|
||||
/**
|
||||
* Base query builder for DELETE queries.
|
||||
* @extends ConnectionMutable
|
||||
* @extends WhereBuilder
|
||||
* @extends TableRefBuilder
|
||||
*/
|
||||
export class Delete<T> extends ConnectionMutable<T> {
|
||||
/**
|
||||
* The target table.
|
||||
* @type QuerySource
|
||||
*/
|
||||
protected _target?: QuerySource = undefined
|
||||
|
||||
/**
|
||||
* The where clauses.
|
||||
* @type Array<WhereStatement>
|
||||
*/
|
||||
protected _wheres: WhereStatement[] = []
|
||||
|
||||
/**
|
||||
* The applied scopes.
|
||||
* @type Array<Scope>
|
||||
*/
|
||||
protected _scopes: Scope[] = []
|
||||
|
||||
/**
|
||||
* The fields to select.
|
||||
* @type Array<string>
|
||||
*/
|
||||
protected _fields: string[] = []
|
||||
|
||||
/**
|
||||
* Include the ONLY operator?
|
||||
* @type boolean
|
||||
*/
|
||||
protected _only: boolean = false
|
||||
|
||||
sql(level = 0): string {
|
||||
@ -29,17 +59,34 @@ export class Delete<T> extends ConnectionMutable<T> {
|
||||
].filter(x => String(x).trim()).join(`\n${indent}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Include the only operator.
|
||||
* @example
|
||||
* SELECT ONLY ...
|
||||
* @return Delete
|
||||
*/
|
||||
only() {
|
||||
this._only = true
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the source to delete from.
|
||||
* @param {QuerySource} source
|
||||
* @param {string} alias
|
||||
* @return Delete
|
||||
*/
|
||||
from(source: QuerySource, alias?: string) {
|
||||
if ( !alias ) this._target = source
|
||||
else this._target = { ref: source, alias }
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the fields to be returned from the query.
|
||||
* @param {...FieldSet} fields
|
||||
* @return Delete
|
||||
*/
|
||||
returning(...fields: FieldSet[]) {
|
||||
for ( const field_set of fields ) {
|
||||
if ( typeof field_set === 'string' ) {
|
||||
|
@ -9,13 +9,30 @@ import {
|
||||
} from '../types.ts'
|
||||
import {HavingBuilderFunction} from './Select.ts'
|
||||
|
||||
/**
|
||||
* Mixin class for queries supporting HAVING clauses.
|
||||
*/
|
||||
export class HavingBuilder {
|
||||
/**
|
||||
* Having clauses to apply to the query.
|
||||
* @type Array<HavingStatement>
|
||||
*/
|
||||
protected _havings: HavingStatement[] = []
|
||||
|
||||
/**
|
||||
* Get the having clauses applied to the query.
|
||||
* @type Array<HavingStatement>
|
||||
*/
|
||||
get having_items() {
|
||||
return this._havings
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast the having statements to SQL.
|
||||
* @param {HavingStatement} [havings]
|
||||
* @param {number} [level = 0] - the indentation level
|
||||
* @return string
|
||||
*/
|
||||
havings_to_sql(havings?: HavingStatement[], level = 0): string {
|
||||
const indent = Array(level * 2).fill(' ').join('')
|
||||
let statements = []
|
||||
@ -30,6 +47,14 @@ export class HavingBuilder {
|
||||
return statements.filter(Boolean).join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper for creating a HAVING clause.
|
||||
* @param {HavingPreOperator} preop
|
||||
* @param {string | HavingBuilderFunction} field
|
||||
* @param {SQLHavingOperator} [operator]
|
||||
* @param [operand]
|
||||
* @private
|
||||
*/
|
||||
private _createHaving(preop: HavingPreOperator, field: string | HavingBuilderFunction, operator?: SQLHavingOperator, operand?: any) {
|
||||
if ( typeof field === 'function' ) {
|
||||
const having_builder = new HavingBuilder()
|
||||
@ -45,11 +70,24 @@ export class HavingBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a basic HAVING clause to the query.
|
||||
* @param {string | HavingBuilderFunction} field
|
||||
* @param {SQLHavingOperator} [operator]
|
||||
* @param [operand]
|
||||
* @return HavingBuilder
|
||||
*/
|
||||
having(field: string | HavingBuilderFunction, operator?: SQLHavingOperator, operand?: any) {
|
||||
this._createHaving('AND', field, operator, operand)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a HAVING ... IN (...) clause to the query.
|
||||
* @param {string} field
|
||||
* @param {EscapedValue} values
|
||||
* @return HavingBuilder
|
||||
*/
|
||||
havingIn(field: string, values: EscapedValue) {
|
||||
this._havings.push({
|
||||
field,
|
||||
@ -60,11 +98,24 @@ export class HavingBuilder {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an HAVING NOT ... clause to the query.
|
||||
* @param {string | HavingBuilderFunction} field
|
||||
* @param {SQLHavingOperator} operator
|
||||
* @param [operand]
|
||||
* @return HavingBuilder
|
||||
*/
|
||||
havingNot(field: string | HavingBuilderFunction, operator?: SQLHavingOperator, operand?: EscapedValue) {
|
||||
this._createHaving('AND NOT', field, operator, operand)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an HAVING NOT ... IN (...) clause to the query.
|
||||
* @param {string} field
|
||||
* @param {EscapedValue} values
|
||||
* @return HavingBuilder
|
||||
*/
|
||||
havingNotIn(field: string, values: EscapedValue) {
|
||||
this._havings.push({
|
||||
field,
|
||||
@ -75,16 +126,36 @@ export class HavingBuilder {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an OR HAVING ... clause to the query.
|
||||
* @param {string | HavingBuilderFunction} field
|
||||
* @param {SQLHavingOperator} [operator]
|
||||
* @param [operand]
|
||||
* @return HavingBuilder
|
||||
*/
|
||||
orHaving(field: string | HavingBuilderFunction, operator?: SQLHavingOperator, operand?: EscapedValue) {
|
||||
this._createHaving('OR', field, operator, operand)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an HAVING OR NOT ... clause to the query.
|
||||
* @param {string | HavingBuilderFunction} field
|
||||
* @param {SQLHavingOperator} [operator]
|
||||
* @param [operand]
|
||||
* @return HavingBuilder
|
||||
*/
|
||||
orHavingNot(field: string | HavingBuilderFunction, operator?: SQLHavingOperator, operand?: EscapedValue) {
|
||||
this._createHaving('OR NOT', field, operator, operand)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an OR HAVING ... IN (...) clause to the query.
|
||||
* @param {string} field
|
||||
* @param {EscapedValue} values
|
||||
* @return HavingBuilder
|
||||
*/
|
||||
orHavingIn(field: string, values: EscapedValue) {
|
||||
this._havings.push({
|
||||
field,
|
||||
@ -95,6 +166,12 @@ export class HavingBuilder {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an OR HAVING NOT ... IN (...) clause to the query.
|
||||
* @param {string} field
|
||||
* @param {EscapedValue} values
|
||||
* @return HavingBuilder
|
||||
*/
|
||||
orHavingNotIn(field: string, values: EscapedValue) {
|
||||
this._havings.push({
|
||||
field,
|
||||
|
@ -8,11 +8,41 @@ import {raw} from '../Builder.ts'
|
||||
|
||||
// TODO support DEFAULT VALUES
|
||||
// TODO support ON CONFLICT
|
||||
|
||||
/**
|
||||
* Query builder base for INSERT queries.
|
||||
* @extends ConnectionMutable
|
||||
* @extends TableRefBuilder
|
||||
*/
|
||||
export class Insert<T> extends ConnectionMutable<T> {
|
||||
/**
|
||||
* The target table to insert into.
|
||||
* @type QuerySource
|
||||
*/
|
||||
protected _target?: QuerySource = undefined
|
||||
|
||||
/**
|
||||
* The columns to insert.
|
||||
* @type Array<string>
|
||||
*/
|
||||
protected _columns: string[] = []
|
||||
|
||||
/**
|
||||
* The row data to insert.
|
||||
* @type Array<string>
|
||||
*/
|
||||
protected _rows: string[] = []
|
||||
|
||||
/**
|
||||
* The fields to insert.
|
||||
* @type Array<string>
|
||||
*/
|
||||
protected _fields: string[] = []
|
||||
|
||||
/**
|
||||
* Return all data?
|
||||
* @type boolean
|
||||
*/
|
||||
protected _return_all = false
|
||||
|
||||
sql(level = 0): string {
|
||||
@ -36,17 +66,33 @@ export class Insert<T> extends ConnectionMutable<T> {
|
||||
].filter(x => String(x).trim()).join(`\n${indent}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the table to insert into.
|
||||
* @param {QuerySource} source
|
||||
* @param {string} [alias]
|
||||
* @return Insert
|
||||
*/
|
||||
into(source: QuerySource, alias?: string) {
|
||||
if ( !alias ) this._target = source
|
||||
else this._target = { ref: source, alias }
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the columns to insert.
|
||||
* @param {...string} columns
|
||||
* @return Insert
|
||||
*/
|
||||
columns(...columns: string[]) {
|
||||
this._columns = columns
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add raw row data to insert.
|
||||
* @param {...EscapedValue} row
|
||||
* @return Insert
|
||||
*/
|
||||
row_raw(...row: EscapedValue[]) {
|
||||
if ( row.length !== this._columns.length )
|
||||
throw new MalformedSQLGrammarError(`Cannot insert row with ${row.length} values using a query that has ${this._columns.length} columns specified.`)
|
||||
@ -55,6 +101,11 @@ export class Insert<T> extends ConnectionMutable<T> {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a field value object to insert.
|
||||
* @param {FieldValueObject} row
|
||||
* @return Insert
|
||||
*/
|
||||
row(row: FieldValueObject) {
|
||||
const columns = []
|
||||
const row_raw = []
|
||||
@ -70,6 +121,11 @@ export class Insert<T> extends ConnectionMutable<T> {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add multiple field value objects to insert.
|
||||
* @param {Array<FieldValueObject>}rows
|
||||
* @return Insert
|
||||
*/
|
||||
rows(rows: FieldValueObject[]) {
|
||||
const [initial, ...rest] = rows
|
||||
|
||||
@ -96,6 +152,11 @@ export class Insert<T> extends ConnectionMutable<T> {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the fields to return after insert.
|
||||
* @param {...FieldSet} fields
|
||||
* @return Insert
|
||||
*/
|
||||
returning(...fields: FieldSet[]) {
|
||||
for ( const field_set of fields ) {
|
||||
if ( typeof field_set === 'string' ) {
|
||||
|
@ -23,24 +23,105 @@ import ConnectionExecutable from './ConnectionExecutable.ts'
|
||||
import {Scope} from '../Scope.ts'
|
||||
import {isInstantiable} from "../../../../di/src/type/Instantiable.ts";
|
||||
|
||||
/**
|
||||
* Base type for functions that operate on WhereBuilders.
|
||||
*/
|
||||
export type WhereBuilderFunction = (group: WhereBuilder) => any
|
||||
|
||||
/**
|
||||
* Base type for functions that operate on HavingBuilders.
|
||||
*/
|
||||
export type HavingBuilderFunction = (group: HavingBuilder) => any
|
||||
|
||||
/**
|
||||
* Base type for functions that operate on Joins.
|
||||
*/
|
||||
export type JoinFunction = (join: Join) => any
|
||||
|
||||
/**
|
||||
* Error class thrown when the SQL generated will be invalid.
|
||||
* @extends Error
|
||||
*/
|
||||
export class MalformedSQLGrammarError extends Error {}
|
||||
|
||||
/**
|
||||
* Query builder base class for SELECT queries.
|
||||
* @extends ConnectionExecutable
|
||||
* @extends TableRefBuilder
|
||||
* @extends WhereBuilder
|
||||
* @extends HavingBuilder
|
||||
*/
|
||||
export class Select<T> extends ConnectionExecutable<T> {
|
||||
/**
|
||||
* The fields to select.
|
||||
* @type Array<string>
|
||||
*/
|
||||
protected _fields: string[] = []
|
||||
|
||||
/**
|
||||
* The source to select from.
|
||||
* @type QuerySource
|
||||
*/
|
||||
protected _source?: QuerySource = undefined
|
||||
|
||||
/**
|
||||
* Where clauses to apply.
|
||||
* @type Array<WhereStatement>
|
||||
*/
|
||||
protected _wheres: WhereStatement[] = []
|
||||
|
||||
/**
|
||||
* The scopes to apply.
|
||||
* @type Array<Scope>
|
||||
*/
|
||||
protected _scopes: Scope[] = []
|
||||
|
||||
/**
|
||||
* Having clauses to apply.
|
||||
* @type Array<HavingStatement>
|
||||
*/
|
||||
protected _havings: HavingStatement[] = []
|
||||
|
||||
/**
|
||||
* Max number of rows to return.
|
||||
* @type number
|
||||
*/
|
||||
protected _limit?: number
|
||||
|
||||
/**
|
||||
* Number of rows to skip.
|
||||
* @type number
|
||||
*/
|
||||
protected _offset?: number
|
||||
|
||||
/**
|
||||
* Join clauses to apply.
|
||||
* @type Array<Join>
|
||||
*/
|
||||
protected _joins: Join[] = []
|
||||
|
||||
/**
|
||||
* Include the DISTINCT operator?
|
||||
* @type boolean
|
||||
*/
|
||||
protected _distinct = false
|
||||
|
||||
/**
|
||||
* Group by clauses to apply.
|
||||
* @type Array<string>
|
||||
*/
|
||||
protected _group_by: string[] = []
|
||||
|
||||
/**
|
||||
* Order by clauses to apply.
|
||||
* @type Array<OrderStatement>
|
||||
*/
|
||||
protected _order: OrderStatement[] = []
|
||||
|
||||
/**
|
||||
* Include the DISTINCT operator.
|
||||
* @return self
|
||||
*/
|
||||
distinct() {
|
||||
this._distinct = true
|
||||
return this
|
||||
@ -73,6 +154,12 @@ export class Select<T> extends ConnectionExecutable<T> {
|
||||
].filter(x => String(x).trim()).join(`\n${indent}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Include a field in the results.
|
||||
* @param {string | Select} field
|
||||
* @param {string} [as] - alias
|
||||
* @return self
|
||||
*/
|
||||
field(field: string | Select<any>, as?: string) {
|
||||
if ( field instanceof Select ) {
|
||||
this._fields.push(`${escape(field)}${as ? ' AS '+as : ''}`)
|
||||
@ -83,11 +170,19 @@ export class Select<T> extends ConnectionExecutable<T> {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the selected fields.
|
||||
* @return self
|
||||
*/
|
||||
clear_fields() {
|
||||
this._fields = []
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a copy of this query.
|
||||
* @return Select
|
||||
*/
|
||||
clone(): Select<T> {
|
||||
const constructor = this.constructor as typeof Select
|
||||
if ( !isInstantiable<Select<T>>(constructor) ) {
|
||||
@ -114,11 +209,21 @@ export class Select<T> extends ConnectionExecutable<T> {
|
||||
return select
|
||||
}
|
||||
|
||||
/**
|
||||
* Add group by clauses to the query.
|
||||
* @param {...string} groupings
|
||||
* @return self
|
||||
*/
|
||||
group_by(...groupings: string[]) {
|
||||
this._group_by = groupings
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Include the given fields in the result set.
|
||||
* @param {...FieldSet} fields
|
||||
* @return self
|
||||
*/
|
||||
fields(...fields: FieldSet[]) {
|
||||
for ( const field_set of fields ) {
|
||||
if ( typeof field_set === 'string' ) {
|
||||
@ -134,72 +239,162 @@ export class Select<T> extends ConnectionExecutable<T> {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the source to select from.
|
||||
* @param {QuerySource} source
|
||||
* @param {string} [alias]
|
||||
* @return self
|
||||
*/
|
||||
from(source: QuerySource, alias?: string) {
|
||||
if ( !alias ) this._source = source
|
||||
else this._source = { ref: source, alias }
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit the returned rows.
|
||||
* @param {number} num
|
||||
* @return self
|
||||
*/
|
||||
limit(num: number) {
|
||||
this._limit = Number(num)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip the first num rows.
|
||||
* @param {number} num
|
||||
* @return self
|
||||
*/
|
||||
offset(num: number) {
|
||||
this._offset = Number(num)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip the first num rows.
|
||||
* @param {number} num
|
||||
* @return self
|
||||
*/
|
||||
skip(num: number) {
|
||||
this._offset = Number(num)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Return only the first num rows.
|
||||
* @param {number} num
|
||||
* @return self
|
||||
*/
|
||||
take(num: number) {
|
||||
this._limit = Number(num)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a JOIN clause to the query by alias, or using a function to build the clause.
|
||||
* @param {QuerySource} source
|
||||
* @param {string | JoinFunction} alias_or_func
|
||||
* @param {JoinFunction} [func]
|
||||
* @return self
|
||||
*/
|
||||
join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) {
|
||||
this._createJoin(Join, source, alias_or_func, func)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a LEFT JOIN clause to the query by alias, or using a function to build the clause.
|
||||
* @param {QuerySource} source
|
||||
* @param {string | JoinFunction} alias_or_func
|
||||
* @param {JoinFunction} [func]
|
||||
* @return self
|
||||
*/
|
||||
left_join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) {
|
||||
this._createJoin(LeftJoin, source, alias_or_func, func)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a LEFT OUTER JOIN clause to the query by alias, or using a function to build the clause.
|
||||
* @param {QuerySource} source
|
||||
* @param {string | JoinFunction} alias_or_func
|
||||
* @param {JoinFunction} [func]
|
||||
* @return self
|
||||
*/
|
||||
left_outer_join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) {
|
||||
this._createJoin(LeftOuterJoin, source, alias_or_func, func)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a CROSS JOIN clause to the query by alias, or using a function to build the clause.
|
||||
* @param {QuerySource} source
|
||||
* @param {string | JoinFunction} alias_or_func
|
||||
* @param {JoinFunction} [func]
|
||||
* @return self
|
||||
*/
|
||||
cross_join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) {
|
||||
this._createJoin(CrossJoin, source, alias_or_func, func)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an INNER JOIN clause to the query by alias, or using a function to build the clause.
|
||||
* @param {QuerySource} source
|
||||
* @param {string | JoinFunction} alias_or_func
|
||||
* @param {JoinFunction} [func]
|
||||
* @return self
|
||||
*/
|
||||
inner_join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) {
|
||||
this._createJoin(InnerJoin, source, alias_or_func, func)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a RIGHT JOIN clause to the query by alias, or using a function to build the clause.
|
||||
* @param {QuerySource} source
|
||||
* @param {string | JoinFunction} alias_or_func
|
||||
* @param {JoinFunction} [func]
|
||||
* @return self
|
||||
*/
|
||||
right_join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) {
|
||||
this._createJoin(RightJoin, source, alias_or_func, func)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a RIGHT OUTER JOIN clause to the query by alias, or using a function to build the clause.
|
||||
* @param {QuerySource} source
|
||||
* @param {string | JoinFunction} alias_or_func
|
||||
* @param {JoinFunction} [func]
|
||||
* @return self
|
||||
*/
|
||||
right_outer_join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) {
|
||||
this._createJoin(RightOuterJoin, source, alias_or_func, func)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a FULL OUTER JOIN clause to the query by alias, or using a function to build the clause.
|
||||
* @param {QuerySource} source
|
||||
* @param {string | JoinFunction} alias_or_func
|
||||
* @param {JoinFunction} [func]
|
||||
* @return self
|
||||
*/
|
||||
full_outer_join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) {
|
||||
this._createJoin(FullOuterJoin, source, alias_or_func, func)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper for creating join clauses using query builder classes.
|
||||
* @param {typeof Join} Class
|
||||
* @param {QuerySource} source
|
||||
* @param {string | JoinFunction} alias_or_func
|
||||
* @param {JoinFunction} [func]
|
||||
* @private
|
||||
*/
|
||||
private _createJoin(Class: typeof Join, source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) {
|
||||
const [table_ref, join_func] = this.join_ref_to_join_args(source, alias_or_func, func)
|
||||
const join = new Class(table_ref)
|
||||
@ -207,6 +402,13 @@ export class Select<T> extends ConnectionExecutable<T> {
|
||||
join_func(join)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast a join reference to the arguments required for the JOIN query builder.
|
||||
* @param {QuerySource} source
|
||||
* @param {string | JoinFunction} alias_or_func
|
||||
* @param {JoinFunction} [func]
|
||||
* @return Array
|
||||
*/
|
||||
join_ref_to_join_args(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction): [TableRef, JoinFunction] {
|
||||
let alias = undefined
|
||||
if ( typeof alias_or_func === 'string' ) alias = alias_or_func
|
||||
@ -221,21 +423,34 @@ export class Select<T> extends ConnectionExecutable<T> {
|
||||
return [this.source_alias_to_table_ref(source, alias), join_func]
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an order by clause to the query.
|
||||
* @param {string} field
|
||||
* @param {string} [direction = 'ASC']
|
||||
* @return self
|
||||
*/
|
||||
order_by(field: string, direction: OrderDirection = 'ASC') {
|
||||
this._order.push({ field, direction })
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an ORDER BY ... ASC clause to the query.
|
||||
* @param {string} field
|
||||
* @return self
|
||||
*/
|
||||
order_asc(field: string) {
|
||||
return this.order_by(field, 'ASC')
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an ORDER BY ... DESC clause to the query.
|
||||
* @param {string} field
|
||||
* @return self
|
||||
*/
|
||||
order_desc(field: string) {
|
||||
return this.order_by(field, 'DESC')
|
||||
}
|
||||
|
||||
// TODO subquery support - https://www.sqlservertutorial.net/sql-server-basics/sql-server-subquery/
|
||||
// TODO raw()
|
||||
}
|
||||
|
||||
export interface Select<T> extends WhereBuilder, TableRefBuilder, HavingBuilder {}
|
||||
|
@ -1,6 +1,14 @@
|
||||
import {TableRef, QuerySource} from '../types.ts'
|
||||
|
||||
/**
|
||||
* Query builder mixin for queries that resolve table names.
|
||||
*/
|
||||
export class TableRefBuilder {
|
||||
/**
|
||||
* Resolve the raw table name to a table reference.
|
||||
* @param {string} from
|
||||
* @return TableRef
|
||||
*/
|
||||
resolve_table_name(from: string): TableRef {
|
||||
const parts = from.split('.')
|
||||
const ref: any = {}
|
||||
@ -20,10 +28,21 @@ export class TableRefBuilder {
|
||||
return ref as TableRef
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a table ref to its raw SQL form.
|
||||
* @param {TableRef} ref
|
||||
* @return string
|
||||
*/
|
||||
serialize_table_ref(ref: TableRef): string {
|
||||
return `${ref.database ? ref.database+'.' : ''}${ref.table}${ref.alias ? ' '+ref.alias : ''}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a query source and alias to a table ref.
|
||||
* @param {QuerySource} source
|
||||
* @param {string} [alias]
|
||||
* @return TableRef
|
||||
*/
|
||||
source_alias_to_table_ref(source: QuerySource, alias?: string) {
|
||||
let string = ''
|
||||
if ( typeof source === 'string' ) {
|
||||
|
@ -1,15 +1,49 @@
|
||||
import ConnectionMutable from "./ConnectionMutable.ts";
|
||||
import {MalformedSQLGrammarError} from "./Select.ts";
|
||||
import {TableRefBuilder} from "./TableRefBuilder.ts";
|
||||
import {applyMixins} from "../../../../lib/src/support/mixins.ts";
|
||||
import {QuerySource} from "../types.ts";
|
||||
import ConnectionMutable from './ConnectionMutable.ts'
|
||||
import {MalformedSQLGrammarError} from './Select.ts'
|
||||
import {TableRefBuilder} from './TableRefBuilder.ts'
|
||||
import {applyMixins} from '../../../../lib/src/support/mixins.ts'
|
||||
import {QuerySource} from '../types.ts'
|
||||
|
||||
/**
|
||||
* Base query builder class for TRUNCATE queries.
|
||||
* @extends ConnectionMutable
|
||||
* @extends TableRefBuilder
|
||||
*/
|
||||
export class Truncate<T> extends ConnectionMutable<T> {
|
||||
/**
|
||||
* The source to be truncated.
|
||||
* @type QuerySource
|
||||
*/
|
||||
protected _source?: QuerySource
|
||||
|
||||
/**
|
||||
* Include the ONLY clause?
|
||||
* @type boolean
|
||||
*/
|
||||
protected _only: boolean = false
|
||||
|
||||
/**
|
||||
* Include the RESTART clause?
|
||||
* @type boolean
|
||||
*/
|
||||
protected _restart: boolean = false
|
||||
|
||||
/**
|
||||
* Include the CONTINUE clause?
|
||||
* @type boolean
|
||||
*/
|
||||
protected _continue: boolean = false
|
||||
|
||||
/**
|
||||
* Include the CASCADE clause?
|
||||
* @type boolean
|
||||
*/
|
||||
protected _cascade: boolean = false
|
||||
|
||||
/**
|
||||
* Include the RESTRICT clause?
|
||||
* @type boolean
|
||||
*/
|
||||
protected _restrict: boolean = false
|
||||
|
||||
constructor(table?: QuerySource, alias?: string) {
|
||||
@ -37,30 +71,52 @@ export class Truncate<T> extends ConnectionMutable<T> {
|
||||
].filter(x => String(x).trim()).join(`\n${indent}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the table to be truncated.
|
||||
* @param {QuerySource} source
|
||||
* @param {string} [alias]
|
||||
* @return self
|
||||
*/
|
||||
table(source: QuerySource, alias?: string) {
|
||||
if ( !alias ) this._source = source
|
||||
else this._source = { ref: source, alias }
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart the ID column. This adds the RESTART clause.
|
||||
* @return self
|
||||
*/
|
||||
restart_identity() {
|
||||
this._continue = false
|
||||
this._restart = true
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Continue the ID column. This adds the CONTINUE clause.
|
||||
* @return self
|
||||
*/
|
||||
continue_identity() {
|
||||
this._continue = true
|
||||
this._restart = false
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the CASCADE clause.
|
||||
* @return self
|
||||
*/
|
||||
cascade() {
|
||||
this._cascade = true
|
||||
this._restrict = false
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the RESTRICT clause.
|
||||
* @return self
|
||||
*/
|
||||
restrict() {
|
||||
this._cascade = false
|
||||
this._restrict = true
|
||||
|
@ -1,4 +1,3 @@
|
||||
import ConnectionExecutable from './ConnectionExecutable.ts'
|
||||
import {escape, EscapedValue, FieldValue, FieldValueObject, QuerySource, WhereStatement, FieldSet} from '../types.ts'
|
||||
import {Collection} from '../../../../lib/src/collection/Collection.ts'
|
||||
import {WhereBuilder} from './WhereBuilder.ts'
|
||||
@ -8,14 +7,49 @@ import {MalformedSQLGrammarError} from './Select.ts'
|
||||
import ConnectionMutable from './ConnectionMutable.ts'
|
||||
import {Scope} from '../Scope.ts'
|
||||
|
||||
// TODO FROM
|
||||
// TODO WHERE CURRENT OF
|
||||
|
||||
/**
|
||||
* Query builder base class for UPDATE queries.
|
||||
* @extends ConnectionMutable
|
||||
* @extends TableRefBuilder
|
||||
* @extends WhereBuilder
|
||||
*/
|
||||
export class Update<T> extends ConnectionMutable<T> {
|
||||
/**
|
||||
* The target table to be updated.
|
||||
* @type QuerySource
|
||||
*/
|
||||
protected _target?: QuerySource = undefined
|
||||
|
||||
/**
|
||||
* Include the ONLY clause?
|
||||
* @type boolean
|
||||
*/
|
||||
protected _only = false
|
||||
|
||||
/**
|
||||
* Field value sets to be updated.
|
||||
* @type Collection<FieldValue>
|
||||
*/
|
||||
protected _sets: Collection<FieldValue> = new Collection<FieldValue>()
|
||||
|
||||
/**
|
||||
* Where clauses to be applied.
|
||||
* @type Array<WhereStatement>
|
||||
*/
|
||||
protected _wheres: WhereStatement[] = []
|
||||
|
||||
/**
|
||||
* Scopes to be applied.
|
||||
* @type Array<Scope>
|
||||
*/
|
||||
protected _scopes: Scope[] = []
|
||||
|
||||
/**
|
||||
* Fields to update.
|
||||
* @type Array<string>
|
||||
*/
|
||||
protected _fields: string[] = []
|
||||
|
||||
sql(level = 0): string {
|
||||
@ -36,22 +70,44 @@ export class Update<T> extends ConnectionMutable<T> {
|
||||
].filter(x => String(x).trim()).join(`\n${indent}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to serialize field value sets to raw SQL.
|
||||
* @param {Collection<FieldValue>} sets
|
||||
* @param {number} level - the indentation level
|
||||
* @return string
|
||||
*/
|
||||
protected serialize_sets(sets: Collection<FieldValue>, level = 0): string {
|
||||
const indent = Array(level * 2).fill(' ').join('')
|
||||
return indent + sets.map(field_value => `${field_value.field} = ${escape(field_value.value)}`).join(`,\n${indent}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Target table to update records in.
|
||||
* @param {QuerySource} source
|
||||
* @param {string} [alias]
|
||||
* @return self
|
||||
*/
|
||||
to(source: QuerySource, alias?: string) {
|
||||
if ( !alias ) this._target = source
|
||||
else this._target = { ref: source, alias }
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the ONLY clause.
|
||||
* @return self
|
||||
*/
|
||||
only() {
|
||||
this._only = true
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a field and value to the update clause.
|
||||
* @param {string} field
|
||||
* @param {EscapedValue} value
|
||||
* @return self
|
||||
*/
|
||||
set(field: string, value: EscapedValue) {
|
||||
const existing = this._sets.firstWhere('field', '=', field)
|
||||
if ( existing ) {
|
||||
@ -62,6 +118,11 @@ export class Update<T> extends ConnectionMutable<T> {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a set of fields and values to the update clause.
|
||||
* @param {FieldValueObject} values
|
||||
* @return self
|
||||
*/
|
||||
data(values: FieldValueObject) {
|
||||
for ( const field in values ) {
|
||||
if ( !values.hasOwnProperty(field) ) continue
|
||||
@ -70,6 +131,11 @@ export class Update<T> extends ConnectionMutable<T> {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the fields to be returned after the update.
|
||||
* @param {...FieldSet} fields
|
||||
* @return self
|
||||
*/
|
||||
returning(...fields: FieldSet[]) {
|
||||
for ( const field_set of fields ) {
|
||||
if ( typeof field_set === 'string' ) {
|
||||
|
@ -4,22 +4,47 @@ import {WhereBuilderFunction} from './Select.ts'
|
||||
import {apply_filter_to_where, QueryFilter} from '../../model/filter.ts'
|
||||
import {Scope} from '../Scope.ts'
|
||||
import {FunctionScope, ScopeFunction} from '../scope/FunctionScope.ts'
|
||||
import {make} from '../../../../di/src/global.ts'
|
||||
import RawValue from '../RawValue.ts'
|
||||
|
||||
/**
|
||||
* Query builder mixin for queries that have WHERE clauses.
|
||||
*/
|
||||
export class WhereBuilder {
|
||||
/**
|
||||
* The where clauses to be applied.
|
||||
* @type Array<WhereStatement>
|
||||
*/
|
||||
protected _wheres: WhereStatement[] = []
|
||||
|
||||
/**
|
||||
* The scopes to be applied.
|
||||
* @type Array<Scope>
|
||||
*/
|
||||
protected _scopes: Scope[] = []
|
||||
|
||||
/**
|
||||
* Get the where clauses applied to the query.
|
||||
* @type Array<WhereStatement>
|
||||
*/
|
||||
get where_items() {
|
||||
return this._wheres
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a scope from this query.
|
||||
* @param {typeof Scope} scope
|
||||
* @return self
|
||||
*/
|
||||
without_scope(scope: typeof Scope) {
|
||||
this._scopes = this._scopes.filter(x => !(x instanceof Scope))
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a scope to this query.
|
||||
* @param {Scope | ScopeFunction} scope
|
||||
* @return self
|
||||
*/
|
||||
with_scope(scope: Scope | ScopeFunction) {
|
||||
if ( scope instanceof Scope ) {
|
||||
this._scopes.push(scope)
|
||||
@ -29,11 +54,21 @@ export class WhereBuilder {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add multiple scopes to this query.
|
||||
* @param {Array<Scope | ScopeFunction>} scopes
|
||||
* @return self
|
||||
*/
|
||||
with_scopes(scopes: (Scope | ScopeFunction)[]) {
|
||||
scopes.forEach(scope => this.with_scope(scope))
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast the where clause to raw SQL.
|
||||
* @param {Array<WhereStatement>} [wheres]
|
||||
* @param {number} [level = 0] - the indentation level
|
||||
*/
|
||||
wheres_to_sql(wheres?: WhereStatement[], level = 0): string {
|
||||
this._scopes.forEach(scope => scope.apply(this))
|
||||
const indent = Array(level * 2).fill(' ').join('')
|
||||
@ -49,6 +84,14 @@ export class WhereBuilder {
|
||||
return statements.filter(Boolean).join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper method for creating where clauses.
|
||||
* @param {WherePreOperator} preop
|
||||
* @param {string | WhereBuilderFunction} field
|
||||
* @param {SQLWhereOperator} [operator]
|
||||
* @param [operand]
|
||||
* @private
|
||||
*/
|
||||
private _createWhere(preop: WherePreOperator, field: string | WhereBuilderFunction, operator?: SQLWhereOperator, operand?: any) {
|
||||
if ( typeof field === 'function' ) {
|
||||
const where_builder = new WhereBuilder()
|
||||
@ -64,21 +107,48 @@ export class WhereBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a basic where clause to the query.
|
||||
* @param {string | WhereBuilderFunction} field
|
||||
* @param {SQLWhereOperator} [operator]
|
||||
* @param [operand]
|
||||
* @return self
|
||||
*/
|
||||
where(field: string | WhereBuilderFunction, operator?: SQLWhereOperator, operand?: any) {
|
||||
this._createWhere('AND', field, operator, operand)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where clause to the query, without escaping the operand.
|
||||
* @param {string | WhereBuilderFunction} field
|
||||
* @param {SQLWhereOperator} [operator]
|
||||
* @param [operand]
|
||||
* @return self
|
||||
*/
|
||||
whereRaw(field: string, operator: SQLWhereOperator, operand: string) {
|
||||
this._createWhere('AND', field, operator, new RawValue(operand))
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an OR WHERE clause to the query, without escaping the operand.
|
||||
* @param {string | WhereBuilderFunction} field
|
||||
* @param {SQLWhereOperator} [operator]
|
||||
* @param [operand]
|
||||
* @return self
|
||||
*/
|
||||
orWhereRaw(field: string, operator: SQLWhereOperator, operand: string) {
|
||||
this._createWhere('OR', field, operator, new RawValue(operand))
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a WHERE ... IN (...) clause to the query.
|
||||
* @param {string} field
|
||||
* @param {EscapedValue} values
|
||||
* @return self
|
||||
*/
|
||||
whereIn(field: string, values: EscapedValue) {
|
||||
this._wheres.push({
|
||||
field,
|
||||
@ -89,11 +159,24 @@ export class WhereBuilder {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a WHERE NOT ... clause to the query.
|
||||
* @param {string | WhereBuilderFunction} field
|
||||
* @param {SQLWhereOperator} [operator]
|
||||
* @param [operand]
|
||||
* @return self
|
||||
*/
|
||||
whereNot(field: string | WhereBuilderFunction, operator?: SQLWhereOperator, operand?: EscapedValue) {
|
||||
this._createWhere('AND NOT', field, operator, operand)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a WHERE ... NOT IN (...) clause to the query.
|
||||
* @param {string} field
|
||||
* @param {EscapedValue} values
|
||||
* @return self
|
||||
*/
|
||||
whereNotIn(field: string, values: EscapedValue) {
|
||||
this._wheres.push({
|
||||
field,
|
||||
@ -104,16 +187,36 @@ export class WhereBuilder {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an OR WHERE ... clause to the query.
|
||||
* @param {string | WhereBuilderFunction} field
|
||||
* @param {SQLWhereOperator} [operator]
|
||||
* @param [operand]
|
||||
* @return self
|
||||
*/
|
||||
orWhere(field: string | WhereBuilderFunction, operator?: SQLWhereOperator, operand?: EscapedValue) {
|
||||
this._createWhere('OR', field, operator, operand)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an OR WHERE NOT clause to the query.
|
||||
* @param {string | WhereBuilderFunction} field
|
||||
* @param {SQLWhereOperator} [operator]
|
||||
* @param [operand]
|
||||
* @return self
|
||||
*/
|
||||
orWhereNot(field: string | WhereBuilderFunction, operator?: SQLWhereOperator, operand?: EscapedValue) {
|
||||
this._createWhere('OR NOT', field, operator, operand)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an OR WHERE ... IN (...) clause to the query.
|
||||
* @param {string} field
|
||||
* @param {EscapedValue} values
|
||||
* @return self
|
||||
*/
|
||||
orWhereIn(field: string, values: EscapedValue) {
|
||||
this._wheres.push({
|
||||
field,
|
||||
@ -124,6 +227,12 @@ export class WhereBuilder {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an OR WHERE ... NOT IN (...) clause to the query.
|
||||
* @param {string} field
|
||||
* @param {EscapedValue} values
|
||||
* @return self
|
||||
*/
|
||||
orWhereNotIn(field: string, values: EscapedValue) {
|
||||
this._wheres.push({
|
||||
field,
|
||||
@ -134,6 +243,13 @@ export class WhereBuilder {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a WHERE ... BETWEEN ... AND ... clause to the query.
|
||||
* @param {string} field
|
||||
* @param {EscapedValue} lower_bound
|
||||
* @param {EscapedValue} upper_bound
|
||||
* @return self
|
||||
*/
|
||||
whereBetween(field: string, lower_bound: EscapedValue, upper_bound: EscapedValue) {
|
||||
this._wheres.push({
|
||||
field,
|
||||
@ -144,6 +260,13 @@ export class WhereBuilder {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an OR WHERE ... BETWEEN ... AND ... clause to the query.
|
||||
* @param {string} field
|
||||
* @param {EscapedValue} lower_bound
|
||||
* @param {EscapedValue} upper_bound
|
||||
* @return self
|
||||
*/
|
||||
orWhereBetween(field: string, lower_bound: EscapedValue, upper_bound: EscapedValue) {
|
||||
this._wheres.push({
|
||||
field,
|
||||
@ -154,6 +277,13 @@ export class WhereBuilder {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a WHERE ... NOT BETWEEN ... AND ... clause to the query.
|
||||
* @param {string} field
|
||||
* @param {EscapedValue} lower_bound
|
||||
* @param {EscapedValue} upper_bound
|
||||
* @return self
|
||||
*/
|
||||
whereNotBetween(field: string, lower_bound: EscapedValue, upper_bound: EscapedValue) {
|
||||
this._wheres.push({
|
||||
field,
|
||||
@ -164,6 +294,13 @@ export class WhereBuilder {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an OR WHERE ... NOT BETWEEN ... AND ... clause to the query.
|
||||
* @param {string} field
|
||||
* @param {EscapedValue} lower_bound
|
||||
* @param {EscapedValue} upper_bound
|
||||
* @return self
|
||||
*/
|
||||
orWhereNotBetween(field: string, lower_bound: EscapedValue, upper_bound: EscapedValue) {
|
||||
this._wheres.push({
|
||||
field,
|
||||
@ -174,6 +311,11 @@ export class WhereBuilder {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a filter object to the query.
|
||||
* @param {QueryFilter} filter
|
||||
* @return self
|
||||
*/
|
||||
filter(filter: QueryFilter) {
|
||||
return apply_filter_to_where(filter, this)
|
||||
}
|
||||
|
@ -1,6 +1,10 @@
|
||||
import {Join} from './Join.ts'
|
||||
import {JoinOperator} from '../../types.ts'
|
||||
|
||||
/**
|
||||
* Query builder class which builds CROSS JOIN statements.
|
||||
* @extends Join
|
||||
*/
|
||||
export class CrossJoin extends Join {
|
||||
public readonly operator: JoinOperator = 'CROSS JOIN'
|
||||
|
||||
|
@ -1,6 +1,10 @@
|
||||
import {Join} from './Join.ts'
|
||||
import {JoinOperator} from '../../types.ts'
|
||||
|
||||
/**
|
||||
* Query builder class which builds FULL OUTER JOINs.
|
||||
* @extends Join
|
||||
*/
|
||||
export class FullOuterJoin extends Join {
|
||||
public readonly operator: JoinOperator = 'FULL OUTER JOIN'
|
||||
}
|
||||
|
@ -1,6 +1,10 @@
|
||||
import {Join} from './Join.ts'
|
||||
import {JoinOperator} from '../../types.ts'
|
||||
|
||||
/**
|
||||
* Query builder class which builds INNER JOIN clauses.
|
||||
* @extends Join
|
||||
*/
|
||||
export class InnerJoin extends Join {
|
||||
public readonly operator: JoinOperator = 'INNER JOIN'
|
||||
}
|
||||
|
@ -4,15 +4,40 @@ import {applyMixins} from '../../../../../lib/src/support/mixins.ts'
|
||||
import {WhereBuilder} from '../WhereBuilder.ts'
|
||||
import {Scope} from '../../Scope.ts'
|
||||
|
||||
/**
|
||||
* Query builder class which builds JOIN clauses.
|
||||
*/
|
||||
export class Join {
|
||||
/**
|
||||
* The join operator to use in the SQL.
|
||||
* @type JoinOperator
|
||||
*/
|
||||
public readonly operator: JoinOperator = 'JOIN'
|
||||
|
||||
/**
|
||||
* The where statements applied to this join. (i.e. JOIN table ON ...)
|
||||
* @type Array<WhereStatement>
|
||||
*/
|
||||
protected _wheres: WhereStatement[] = []
|
||||
|
||||
/**
|
||||
* The scopes applied to this join.
|
||||
* @type Array<Scope>
|
||||
*/
|
||||
protected _scopes: Scope[] = []
|
||||
|
||||
constructor(
|
||||
/**
|
||||
* The table ref being joined.
|
||||
* @type TableRef
|
||||
*/
|
||||
public readonly table_ref: TableRef
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Serialize the join to raw SQL.
|
||||
* @param level
|
||||
*/
|
||||
sql(level = 0): string {
|
||||
const indent = Array(level * 2).fill(' ').join('')
|
||||
return [
|
||||
|
@ -1,6 +1,10 @@
|
||||
import {Join} from './Join.ts'
|
||||
import {JoinOperator} from '../../types.ts'
|
||||
|
||||
/**
|
||||
* Query builder class which creates LEFT JOIN clauses.
|
||||
* @extends Join
|
||||
*/
|
||||
export class LeftJoin extends Join {
|
||||
public readonly operator: JoinOperator = 'LEFT JOIN'
|
||||
}
|
||||
|
@ -1,6 +1,10 @@
|
||||
import {Join} from './Join.ts'
|
||||
import {JoinOperator} from '../../types.ts'
|
||||
|
||||
/**
|
||||
* Query builder class which creates LEFT OUTER JOIN clauses.
|
||||
* @extends Join
|
||||
*/
|
||||
export class LeftOuterJoin extends Join {
|
||||
public readonly operator: JoinOperator = 'LEFT OUTER JOIN'
|
||||
}
|
||||
|
@ -1,6 +1,10 @@
|
||||
import {Join} from './Join.ts'
|
||||
import {JoinOperator} from '../../types.ts'
|
||||
|
||||
/**
|
||||
* Query builder class which creates RIGHT JOIN clauses.
|
||||
* @extends Join
|
||||
*/
|
||||
export class RightJoin extends Join {
|
||||
public readonly operator: JoinOperator = 'RIGHT JOIN'
|
||||
}
|
||||
|
@ -1,6 +1,10 @@
|
||||
import {Join} from './Join.ts'
|
||||
import {JoinOperator} from '../../types.ts'
|
||||
|
||||
/**
|
||||
* Query builder class which creates RIGHT OUTER JOIN clauses.
|
||||
* @extends Join
|
||||
*/
|
||||
export class RightOuterJoin extends Join {
|
||||
public readonly operator: JoinOperator = 'RIGHT OUTER JOIN'
|
||||
}
|
||||
|
@ -1,6 +1,10 @@
|
||||
import ResultOperator from './ResultOperator.ts'
|
||||
import {QueryRow} from '../../../db/types.ts'
|
||||
|
||||
/**
|
||||
* Basic result operator which returns query results as object values.
|
||||
* @extends ResultOperator
|
||||
*/
|
||||
export default class ObjectResultOperator extends ResultOperator<QueryRow> {
|
||||
|
||||
inflate_row(row: QueryRow): QueryRow {
|
||||
|
@ -2,6 +2,10 @@ import {AsyncCollection} from '../../../../../lib/src/collection/AsyncCollection
|
||||
import {ResultIterable} from './ResultIterable.ts'
|
||||
import {Collection} from '../../../../../lib/src/collection/Collection.ts'
|
||||
|
||||
/**
|
||||
* Asynchronous collection representing the results of a query.
|
||||
* @extends AsyncCollection
|
||||
*/
|
||||
export class ResultCollection<T> extends AsyncCollection<T> {
|
||||
constructor(
|
||||
executable: ResultIterable<T>,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user