JSDoc all the things!

This commit is contained in:
Garrett Mills 2020-08-17 09:44:23 -05:00
parent c2a7c3f914
commit f67ae37923
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246
121 changed files with 2855 additions and 63 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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' ) {

View File

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

View File

@ -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>()

View File

@ -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)

View File

@ -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 {
}

View File

@ -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:

View File

@ -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'

View File

@ -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)

View File

@ -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;
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -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>;
}

View File

@ -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({})

View File

@ -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) {

View File

@ -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>
}

View File

@ -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 {

View File

@ -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

View File

@ -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) => {}

View File

@ -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)

View File

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

View File

@ -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'

View File

@ -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'

View File

@ -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)

View File

@ -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

View File

@ -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) ) {

View File

@ -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'

View File

@ -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('.')

View File

@ -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'

View File

@ -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)

View File

@ -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())

View File

@ -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'

View File

@ -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) ) {

View File

@ -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)

View File

@ -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(

View File

@ -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'

View File

@ -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')
}

View File

@ -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
) {}
}

View File

@ -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
}

View File

@ -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()

View File

@ -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()
}

View File

@ -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

View File

@ -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()

View File

@ -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' ) {

View File

@ -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,

View File

@ -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' ) {

View File

@ -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 {}

View File

@ -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' ) {

View File

@ -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

View File

@ -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' ) {

View File

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

View File

@ -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'

View File

@ -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'
}

View File

@ -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'
}

View File

@ -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 [

View File

@ -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'
}

View File

@ -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'
}

View File

@ -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'
}

View File

@ -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'
}

View File

@ -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 {

View File

@ -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