diff --git a/lib/src/http/ApiController.ts b/lib/src/http/ApiController.ts index 1ff1ce3..62e983f 100644 --- a/lib/src/http/ApiController.ts +++ b/lib/src/http/ApiController.ts @@ -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 diff --git a/lib/src/http/Controller.ts b/lib/src/http/Controller.ts index 69bd5fe..e9d62a0 100644 --- a/lib/src/http/Controller.ts +++ b/lib/src/http/Controller.ts @@ -1,5 +1,9 @@ import AppClass from '../lifecycle/AppClass.ts' +/** + * Base class for an HTTP controller. + * @extends AppClass + */ export default class Controller extends AppClass { } diff --git a/lib/src/http/CookieJar.ts b/lib/src/http/CookieJar.ts index 3873b84..3f93ada 100644 --- a/lib/src/http/CookieJar.ts +++ b/lib/src/http/CookieJar.ts @@ -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 { return this._parsed[key] } + /** + * Get the parsed value of a cookie, if it is defined. + * @param {string} key + * @return Promise + */ public async get(key: string): Promise { // 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 + */ public async set(key: string, value: any): Promise { 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 + */ public async has(key: string): Promise { return (await this._cache.has(key)) || key in this._parsed } + /** + * Deletes the given cookie, if it exists. + * @param {string} key + * @return Promise + */ public async delete(key: string): Promise { await this._cache.drop(key) delCookie(this.request.response, key) diff --git a/lib/src/http/Middleware.ts b/lib/src/http/Middleware.ts index 3d26ad0..27ced0e 100644 --- a/lib/src/http/Middleware.ts +++ b/lib/src/http/Middleware.ts @@ -1,5 +1,9 @@ import AppClass from '../lifecycle/AppClass.ts' +/** + * Base class for HTTP middleware. + * @extends AppClass + */ export default class Middleware extends AppClass { } diff --git a/lib/src/http/Request.ts b/lib/src/http/Request.ts index 27daf18..6b40b99 100644 --- a/lib/src/http/Request.ts +++ b/lib/src/http/Request.ts @@ -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 + */ 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' } diff --git a/lib/src/http/Response.ts b/lib/src/http/Response.ts index d01513d..7772ddc 100644 --- a/lib/src/http/Response.ts +++ b/lib/src/http/Response.ts @@ -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) diff --git a/lib/src/http/SecureRequest.ts b/lib/src/http/SecureRequest.ts index 1752ba6..34dc383 100644 --- a/lib/src/http/SecureRequest.ts +++ b/lib/src/http/SecureRequest.ts @@ -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 } diff --git a/lib/src/http/response/DehydratedStateResponseFactory.ts b/lib/src/http/response/DehydratedStateResponseFactory.ts index d1519d2..816b580 100644 --- a/lib/src/http/response/DehydratedStateResponseFactory.ts +++ b/lib/src/http/response/DehydratedStateResponseFactory.ts @@ -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() diff --git a/lib/src/http/response/ErrorResponseFactory.ts b/lib/src/http/response/ErrorResponseFactory.ts index e4aa248..ec37510 100644 --- a/lib/src/http/response/ErrorResponseFactory.ts +++ b/lib/src/http/response/ErrorResponseFactory.ts @@ -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 = 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 ` Sorry, an unexpected error occurred while processing your request. diff --git a/lib/src/http/response/HTMLResponseFactory.ts b/lib/src/http/response/HTMLResponseFactory.ts index 78205a1..1b017c8 100644 --- a/lib/src/http/response/HTMLResponseFactory.ts +++ b/lib/src/http/response/HTMLResponseFactory.ts @@ -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, diff --git a/lib/src/http/response/HTTPErrorResponseFactory.ts b/lib/src/http/response/HTTPErrorResponseFactory.ts index dcc7127..f9005b5 100644 --- a/lib/src/http/response/HTTPErrorResponseFactory.ts +++ b/lib/src/http/response/HTTPErrorResponseFactory.ts @@ -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) diff --git a/lib/src/http/response/JSONResponseFactory.ts b/lib/src/http/response/JSONResponseFactory.ts index 2ff97ad..f6bd40e 100644 --- a/lib/src/http/response/JSONResponseFactory.ts +++ b/lib/src/http/response/JSONResponseFactory.ts @@ -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() diff --git a/lib/src/http/response/PartialViewResponseFactory.ts b/lib/src/http/response/PartialViewResponseFactory.ts index b8ad437..7b738a7 100644 --- a/lib/src/http/response/PartialViewResponseFactory.ts +++ b/lib/src/http/response/PartialViewResponseFactory.ts @@ -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() diff --git a/lib/src/http/response/ResponseFactory.ts b/lib/src/http/response/ResponseFactory.ts index 0e16d1c..f74502e 100644 --- a/lib/src/http/response/ResponseFactory.ts +++ b/lib/src/http/response/ResponseFactory.ts @@ -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 + */ public async write(request: Request): Promise { 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 diff --git a/lib/src/http/response/StringResponseFactory.ts b/lib/src/http/response/StringResponseFactory.ts index 541d752..4751e03 100644 --- a/lib/src/http/response/StringResponseFactory.ts +++ b/lib/src/http/response/StringResponseFactory.ts @@ -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() diff --git a/lib/src/http/response/TemporaryRedirectResponseFactory.ts b/lib/src/http/response/TemporaryRedirectResponseFactory.ts index c196367..4fc78ba 100644 --- a/lib/src/http/response/TemporaryRedirectResponseFactory.ts +++ b/lib/src/http/response/TemporaryRedirectResponseFactory.ts @@ -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() diff --git a/lib/src/http/response/ViewResponseFactory.ts b/lib/src/http/response/ViewResponseFactory.ts index 8ad6cda..ca57df9 100644 --- a/lib/src/http/response/ViewResponseFactory.ts +++ b/lib/src/http/response/ViewResponseFactory.ts @@ -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() diff --git a/lib/src/http/response/helpers.ts b/lib/src/http/response/helpers.ts index 247566f..e2dd320 100644 --- a/lib/src/http/response/helpers.ts +++ b/lib/src/http/response/helpers.ts @@ -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) } diff --git a/lib/src/http/routing/ActivatedRoute.ts b/lib/src/http/routing/ActivatedRoute.ts index 156ff11..cfcf851 100644 --- a/lib/src/http/routing/ActivatedRoute.ts +++ b/lib/src/http/routing/ActivatedRoute.ts @@ -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() diff --git a/lib/src/http/routing/ComplexRoute.ts b/lib/src/http/routing/ComplexRoute.ts index 31cea51..19301f9 100644 --- a/lib/src/http/routing/ComplexRoute.ts +++ b/lib/src/http/routing/ComplexRoute.ts @@ -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) diff --git a/lib/src/http/routing/DeepmatchRoute.ts b/lib/src/http/routing/DeepmatchRoute.ts index f1120ac..c0fde17 100644 --- a/lib/src/http/routing/DeepmatchRoute.ts +++ b/lib/src/http/routing/DeepmatchRoute.ts @@ -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} 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 _ diff --git a/lib/src/http/routing/RegExRoute.ts b/lib/src/http/routing/RegExRoute.ts index 7e4403f..1fabc2a 100644 --- a/lib/src/http/routing/RegExRoute.ts +++ b/lib/src/http/routing/RegExRoute.ts @@ -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}`) diff --git a/lib/src/http/routing/Route.ts b/lib/src/http/routing/Route.ts index 18d57b0..ae5bf6d 100644 --- a/lib/src/http/routing/Route.ts +++ b/lib/src/http/routing/Route.ts @@ -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} + */ 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) diff --git a/lib/src/http/routing/SimpleRoute.ts b/lib/src/http/routing/SimpleRoute.ts index f832625..10c9374 100644 --- a/lib/src/http/routing/SimpleRoute.ts +++ b/lib/src/http/routing/SimpleRoute.ts @@ -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() diff --git a/lib/src/http/session/MemorySession.ts b/lib/src/http/session/MemorySession.ts index 86a165c..7f1a3d0 100644 --- a/lib/src/http/session/MemorySession.ts +++ b/lib/src/http/session/MemorySession.ts @@ -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 = {} diff --git a/lib/src/http/session/MemorySessionFactory.ts b/lib/src/http/session/MemorySessionFactory.ts index be2d4e1..db03ef2 100644 --- a/lib/src/http/session/MemorySessionFactory.ts +++ b/lib/src/http/session/MemorySessionFactory.ts @@ -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() diff --git a/lib/src/http/session/MemorySessionManager.ts b/lib/src/http/session/MemorySessionManager.ts index a717414..3fb0eaa 100644 --- a/lib/src/http/session/MemorySessionManager.ts +++ b/lib/src/http/session/MemorySessionManager.ts @@ -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 + */ private _sessions: Collection = new Collection() public async has_session(key: string): Promise { diff --git a/lib/src/http/session/MemorySessionManagerFactory.ts b/lib/src/http/session/MemorySessionManagerFactory.ts index 097e8cf..f49f4ac 100644 --- a/lib/src/http/session/MemorySessionManagerFactory.ts +++ b/lib/src/http/session/MemorySessionManagerFactory.ts @@ -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() diff --git a/lib/src/http/session/ModelSessionFactory.ts b/lib/src/http/session/ModelSessionFactory.ts index ff118e0..8b2734f 100644 --- a/lib/src/http/session/ModelSessionFactory.ts +++ b/lib/src/http/session/ModelSessionFactory.ts @@ -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 + */ protected readonly ModelClass: StaticClass, ) { super() diff --git a/lib/src/http/session/ModelSessionManager.ts b/lib/src/http/session/ModelSessionManager.ts index 98cb1b6..741670b 100644 --- a/lib/src/http/session/ModelSessionManager.ts +++ b/lib/src/http/session/ModelSessionManager.ts @@ -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 + */ protected readonly ModelClass: StaticClass, ) { super() diff --git a/lib/src/http/session/ModelSessionManagerFactory.ts b/lib/src/http/session/ModelSessionManagerFactory.ts index de5fbfe..5e1a888 100644 --- a/lib/src/http/session/ModelSessionManagerFactory.ts +++ b/lib/src/http/session/ModelSessionManagerFactory.ts @@ -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 + */ protected readonly ModelClass: StaticClass, ) { super() diff --git a/lib/src/http/session/Session.ts b/lib/src/http/session/Session.ts index a3f0585..21d84fc 100644 --- a/lib/src/http/session/Session.ts +++ b/lib/src/http/session/Session.ts @@ -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 + */ public abstract async persist(): Promise + + /** + * 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 + */ public abstract async init_session(): Promise } diff --git a/lib/src/http/session/SessionFactory.ts b/lib/src/http/session/SessionFactory.ts index 04e11c3..2f146d4 100644 --- a/lib/src/http/session/SessionFactory.ts +++ b/lib/src/http/session/SessionFactory.ts @@ -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({}) diff --git a/lib/src/http/session/SessionInterface.ts b/lib/src/http/session/SessionInterface.ts index d18bfae..4321370 100644 --- a/lib/src/http/session/SessionInterface.ts +++ b/lib/src/http/session/SessionInterface.ts @@ -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 } +/** + * 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 }, diff --git a/lib/src/http/session/SessionManager.ts b/lib/src/http/session/SessionManager.ts index 878703d..3e6e0a0 100644 --- a/lib/src/http/session/SessionManager.ts +++ b/lib/src/http/session/SessionManager.ts @@ -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 + */ public abstract async get_session(key?: string): Promise + + /** + * Returns true if the manager has a session with the given key. + * @param {string} key + * @return Promise + */ public abstract async has_session(key: string): Promise + + /** + * Purge a session by key, if provided, or all sessions. + * @param {string} key + * @return Promise + */ public abstract async purge(key?: string): Promise } diff --git a/lib/src/http/session/SessionManagerFactory.ts b/lib/src/http/session/SessionManagerFactory.ts index c7a7a0b..904aff4 100644 --- a/lib/src/http/session/SessionManagerFactory.ts +++ b/lib/src/http/session/SessionManagerFactory.ts @@ -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({}) diff --git a/lib/src/http/session/SessionModel.ts b/lib/src/http/session/SessionModel.ts index 04aa1ae..eee2aa8 100644 --- a/lib/src/http/session/SessionModel.ts +++ b/lib/src/http/session/SessionModel.ts @@ -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 + * @implements SessionInterface + */ export default class SessionModel extends Model implements SessionInterface { protected static populate_key_on_insert: boolean = true + /** + * The JSON serialized session data. + * @type string + */ @Field(Type.json) protected data?: string diff --git a/lib/src/http/type/HTTPRequest.ts b/lib/src/http/type/HTTPRequest.ts index 3c35dda..e52d11f 100644 --- a/lib/src/http/type/HTTPRequest.ts +++ b/lib/src/http/type/HTTPRequest.ts @@ -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 diff --git a/lib/src/http/type/HTTPResponse.ts b/lib/src/http/type/HTTPResponse.ts index 10e7b67..4c0bc3a 100644 --- a/lib/src/http/type/HTTPResponse.ts +++ b/lib/src/http/type/HTTPResponse.ts @@ -1,5 +1,8 @@ import {CookieJar} from '../CookieJar.ts' +/** + * Base type for an outgoing HTTP response. + */ export interface HTTPResponse { status: number headers: Headers diff --git a/lib/src/http/type/RouterDefinition.ts b/lib/src/http/type/RouterDefinition.ts index b3852fd..3d53963 100644 --- a/lib/src/http/type/RouterDefinition.ts +++ b/lib/src/http/type/RouterDefinition.ts @@ -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.') diff --git a/lib/src/lifecycle/AppClass.ts b/lib/src/lifecycle/AppClass.ts index 597014c..9a64aca 100644 --- a/lib/src/lifecycle/AppClass.ts +++ b/lib/src/lifecycle/AppClass.ts @@ -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(target: Instantiable|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(target: Instantiable|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' ) { diff --git a/lib/src/lifecycle/Application.ts b/lib/src/lifecycle/Application.ts index 4bb2551..6720959 100644 --- a/lib/src/lifecycle/Application.ts +++ b/lib/src/lifecycle/Application.ts @@ -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 + */ protected instantiated_units: Collection = new Collection() constructor( protected logger: Logging, protected rleh: RunLevelErrorHandler, + /** + * Array of unit classes to run for this application. + * @type Array> + */ protected units: (Instantiable)[], ) {} + /** + * 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 + */ 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 + */ async down() { } + /** + * Run the application. + * @return Promise + */ 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) } diff --git a/lib/src/lifecycle/Unit.ts b/lib/src/lifecycle/Unit.ts index 50e6e49..ce094b1 100644 --- a/lib/src/lifecycle/Unit.ts +++ b/lib/src/lifecycle/Unit.ts @@ -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 + */ public async up(): Promise {}; + + /** + * Method called to stop the unit when the application is stopping. + * @return Promise + */ public async down(): Promise {}; + /** + * Returns a collection of lifecycle units that this lifecycle unit depends on. + * @return Collection + */ public static get_dependencies(): Collection { if ( isInstantiable(this) ) { const deps = new Collection() diff --git a/lib/src/lifecycle/decorators.ts b/lib/src/lifecycle/decorators.ts index 82ff42f..32e38c4 100644 --- a/lib/src/lifecycle/decorators.ts +++ b/lib/src/lifecycle/decorators.ts @@ -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) diff --git a/lib/src/service/ServiceProvider.ts b/lib/src/service/ServiceProvider.ts index e1caf2c..e1578b6 100644 --- a/lib/src/service/ServiceProvider.ts +++ b/lib/src/service/ServiceProvider.ts @@ -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 { } diff --git a/lib/src/service/logging/Logger.ts b/lib/src/service/logging/Logger.ts index ba9c34d..bb87bca 100644 --- a/lib/src/service/logging/Logger.ts +++ b/lib/src/service/logging/Logger.ts @@ -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 + */ public abstract async write(message: LogMessage): Promise; + /** + * 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: diff --git a/lib/src/service/logging/Logging.ts b/lib/src/service/logging/Logging.ts index 51b120b..3dc2679 100644 --- a/lib/src/service/logging/Logging.ts +++ b/lib/src/service/logging/Logging.ts @@ -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 + */ 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' diff --git a/lib/src/service/logging/StandardLogger.ts b/lib/src/service/logging/StandardLogger.ts index f4afbd6..3909aeb 100644 --- a/lib/src/service/logging/StandardLogger.ts +++ b/lib/src/service/logging/StandardLogger.ts @@ -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 { const prefix = this.level_display(message.level) diff --git a/lib/src/service/logging/types.ts b/lib/src/service/logging/types.ts index dfff201..e9eedcd 100644 --- a/lib/src/service/logging/types.ts +++ b/lib/src/service/logging/types.ts @@ -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; } diff --git a/lib/src/service/utility/Utility.ts b/lib/src/service/utility/Utility.ts index e2bbcbe..5f8549e 100644 --- a/lib/src/service/utility/Utility.ts +++ b/lib/src/service/utility/Utility.ts @@ -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(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() } diff --git a/lib/src/support/BehaviorSubject.ts b/lib/src/support/BehaviorSubject.ts index 8e39fba..a302d32 100644 --- a/lib/src/support/BehaviorSubject.ts +++ b/lib/src/support/BehaviorSubject.ts @@ -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 = (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 = (val?: T) => any +/** + * Subscribers that define multiple handler methods. + */ export type ComplexSubscriber = { next?: SubscriberFunction, error?: SubscriberErrorFunction, complete?: SubscriberCompleteFunction, } +/** + * Subscription to a behavior subject. + */ export type Subscription = SubscriberFunction | ComplexSubscriber + +/** + * Object providing helpers for unsubscribing from a subscription. + */ export type Unsubscribe = { unsubscribe: () => void } +/** + * A stream-based state class. + */ export class BehaviorSubject { + /** + * Subscribers to this subject. + * @type Array + */ protected subscribers: ComplexSubscriber[] = [] + + /** + * 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): Unsubscribe { if ( typeof subscriber === 'function' ) { this.subscribers.push({ next: subscriber }) @@ -38,6 +94,10 @@ export class BehaviorSubject { } } + /** + * Cast this subject to a promise, which resolves on the output of the next value. + * @return Promise + */ public to_promise(): Promise { return new Promise((resolve, reject) => { const { unsubscribe } = this.subscribe({ @@ -57,6 +117,11 @@ export class BehaviorSubject { }) } + /** + * Push a new value to this subject. The promise resolves when all subscribers have been pushed to. + * @param val + * @return Promise + */ public async next(val: T): Promise { if ( this._is_complete ) throw new CompletedObservableError() this._value = val @@ -78,11 +143,23 @@ export class BehaviorSubject { } } + /** + * 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 + */ public async push(vals: T[]): Promise { 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 + */ public async complete(final_val?: T): Promise { if ( this._is_complete ) throw new CompletedObservableError() if ( typeof final_val === 'undefined' ) final_val = this.value() @@ -105,10 +182,17 @@ export class BehaviorSubject { 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 } diff --git a/lib/src/support/Cache.ts b/lib/src/support/Cache.ts index 2bf442c..10ae2a4 100644 --- a/lib/src/support/Cache.ts +++ b/lib/src/support/Cache.ts @@ -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 + */ public abstract async fetch(key: string): Promise; + + /** + * Store the given value in the cache by key. + * @param {string} key + * @param {string} value + */ public abstract async put(key: string, value: string): Promise; + + /** + * Check if the cache has the given key. + * @param {string} key + * @return Promise + */ public abstract async has(key: string): Promise; + + /** + * Drop the given key from the cache. + * @param {string} key + */ public abstract async drop(key: string): Promise; } diff --git a/lib/src/support/CacheFactory.ts b/lib/src/support/CacheFactory.ts index 7f79622..ca0c302 100644 --- a/lib/src/support/CacheFactory.ts +++ b/lib/src/support/CacheFactory.ts @@ -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({}) diff --git a/lib/src/support/InMemCache.ts b/lib/src/support/InMemCache.ts index 54c9ebc..864317e 100644 --- a/lib/src/support/InMemCache.ts +++ b/lib/src/support/InMemCache.ts @@ -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 + */ protected items: Collection = new Collection() public async fetch(key: string) { diff --git a/lib/src/support/Rehydratable.ts b/lib/src/support/Rehydratable.ts index e2214c8..ee49a99 100644 --- a/lib/src/support/Rehydratable.ts +++ b/lib/src/support/Rehydratable.ts @@ -1,6 +1,13 @@ - +/** + * Type representing a JSON serializable object. + */ export type JSONState = { [key: string]: string | boolean | number | undefined | JSONState | Array } +/** + * 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 + */ dehydrate(): Promise + + /** + * Rehydrate a state into this class. + * @param {JSONState} state + * @return void|Promise + */ rehydrate(state: JSONState): void | Promise } diff --git a/lib/src/support/api.ts b/lib/src/support/api.ts index 139997a..1397f8e 100644 --- a/lib/src/support/api.ts +++ b/lib/src/support/api.ts @@ -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 { diff --git a/lib/src/support/mixins.ts b/lib/src/support/mixins.ts index 72c282b..203cba2 100644 --- a/lib/src/support/mixins.ts +++ b/lib/src/support/mixins.ts @@ -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 = new (...args: any[]) => T diff --git a/lib/src/support/timeout.ts b/lib/src/support/timeout.ts index c1e71d3..d8713d3 100644 --- a/lib/src/support/timeout.ts +++ b/lib/src/support/timeout.ts @@ -1,3 +1,6 @@ +/** + * Base interface representing a timeout subscriber. + */ export interface TimeoutSubscriber { on_time: (handler: (arg: T) => any) => TimeoutSubscriber, late: (handler: (arg: T) => any) => TimeoutSubscriber, @@ -5,6 +8,11 @@ export interface TimeoutSubscriber { run: () => Promise, } +/** + * Subscribe to a promise with a timeout. + * @param {number} timeout - timeout in milliseconds + * @param {Promise} promise - the promise to subscribe to + */ export function withTimeout(timeout: number, promise: Promise) { let on_time_handler: (arg: T) => any = (arg) => {} let late_handler: (arg: T) => any = (arg) => {} diff --git a/lib/src/unit/Canon.ts b/lib/src/unit/Canon.ts index 55ce172..b2e4ead 100644 --- a/lib/src/unit/Canon.ts +++ b/lib/src/unit/Canon.ts @@ -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 } = {} + /** + * Get a canonical resource by its name key. + * @param {string} key + * @return Canonical + */ resource(key: string): Canonical { if ( !this.resources[key] ) throw new NoSuchCanonicalResolverKeyError(key) return this.resources[key] as Canonical } + /** + * Register a canonical resource. + * @param {Canonical} unit + */ register_canonical(unit: Canonical) { const key = unit.canonical_items if ( this.resources[key] ) throw new DuplicateResolverKeyError(key) diff --git a/lib/src/unit/Canonical.ts b/lib/src/unit/Canonical.ts index d8a5084..8bd2129 100644 --- a/lib/src/unit/Canonical.ts +++ b/lib/src/unit/Canonical.ts @@ -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 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 extends LifecycleUnit { } } + /** + * Get an array of all canonical reference names. + * @return Array + */ 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 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 + */ public async init_canonical_item(definition: CanonicalDefinition): Promise { return definition.imported.default } + /** + * Given a file path, build the canonical definition represented by that path. + * @param {string} file_path + * @private + * @return Promise + */ private async _get_canonical_definition(file_path: string): Promise { 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 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] } diff --git a/lib/src/unit/Config.ts b/lib/src/unit/Config.ts index b702314..aafd82b 100644 --- a/lib/src/unit/Config.ts +++ b/lib/src/unit/Config.ts @@ -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' diff --git a/lib/src/unit/Controllers.ts b/lib/src/unit/Controllers.ts index ad780b2..b658030 100644 --- a/lib/src/unit/Controllers.ts +++ b/lib/src/unit/Controllers.ts @@ -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 { protected base_path = './app/http/controllers' diff --git a/lib/src/unit/HttpKernel.ts b/lib/src/unit/HttpKernel.ts index d047d49..f659cf1 100644 --- a/lib/src/unit/HttpKernel.ts +++ b/lib/src/unit/HttpKernel.ts @@ -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) diff --git a/lib/src/unit/HttpServer.ts b/lib/src/unit/HttpServer.ts index 1f7dc42..cc9e3e7 100644 --- a/lib/src/unit/HttpServer.ts +++ b/lib/src/unit/HttpServer.ts @@ -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 diff --git a/lib/src/unit/InstantiableCanonical.ts b/lib/src/unit/InstantiableCanonical.ts index dd423b1..6a48678 100644 --- a/lib/src/unit/InstantiableCanonical.ts +++ b/lib/src/unit/InstantiableCanonical.ts @@ -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 extends Canonical> { public async init_canonical_item(def: CanonicalDefinition): Promise> { if ( isInstantiable(def.imported.default) ) { diff --git a/lib/src/unit/Middlewares.ts b/lib/src/unit/Middlewares.ts index 5c515e6..27e7289 100644 --- a/lib/src/unit/Middlewares.ts +++ b/lib/src/unit/Middlewares.ts @@ -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 { protected base_path = './app/http/middleware' diff --git a/lib/src/unit/RecursiveCanonical.ts b/lib/src/unit/RecursiveCanonical.ts index 8944616..36c828b 100644 --- a/lib/src/unit/RecursiveCanonical.ts +++ b/lib/src/unit/RecursiveCanonical.ts @@ -1,5 +1,8 @@ import {Canonical} from './Canonical.ts' +/** + * Special canonical unit which deep-resolves values recursively. + */ export class RecursiveCanonical extends Canonical { public get(key: string, fallback?: any): any | undefined { const parts = key.split('.') diff --git a/lib/src/unit/Routes.ts b/lib/src/unit/Routes.ts index ab28866..e801ca9 100644 --- a/lib/src/unit/Routes.ts +++ b/lib/src/unit/Routes.ts @@ -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 { protected base_path = './app/http/routes' diff --git a/lib/src/unit/Routing.ts b/lib/src/unit/Routing.ts index 3e9f201..7429bb2 100644 --- a/lib/src/unit/Routing.ts +++ b/lib/src/unit/Routing.ts @@ -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 | ResponseFactory | Promise | void | Promise + +/** + * 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 + */ protected instances: Collection = new Collection() 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} 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} 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) diff --git a/lib/src/unit/Scaffolding.ts b/lib/src/unit/Scaffolding.ts index 469f64d..f6b7603 100644 --- a/lib/src/unit/Scaffolding.ts +++ b/lib/src/unit/Scaffolding.ts @@ -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()) diff --git a/lib/src/unit/Services.ts b/lib/src/unit/Services.ts index edd54b4..b0f710e 100644 --- a/lib/src/unit/Services.ts +++ b/lib/src/unit/Services.ts @@ -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 { protected base_path = './app/services' protected canonical_item = 'service' diff --git a/lib/src/unit/StaticCanonical.ts b/lib/src/unit/StaticCanonical.ts index 00cf525..29e62ba 100644 --- a/lib/src/unit/StaticCanonical.ts +++ b/lib/src/unit/StaticCanonical.ts @@ -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 extends Canonical> { public async init_canonical_item(def: CanonicalDefinition): Promise> { if ( isStaticClass(def.imported.default) ) { diff --git a/lib/src/unit/ViewEngine.ts b/lib/src/unit/ViewEngine.ts index 4b96649..63ae94f 100644 --- a/lib/src/unit/ViewEngine.ts +++ b/lib/src/unit/ViewEngine.ts @@ -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 + */ async render(view: string, args?: any, layout?: string): Promise { 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) diff --git a/orm/src/DatabaseUnit.ts b/orm/src/DatabaseUnit.ts index d50197b..e0c4c68 100644 --- a/orm/src/DatabaseUnit.ts +++ b/orm/src/DatabaseUnit.ts @@ -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( diff --git a/orm/src/ModelsUnit.ts b/orm/src/ModelsUnit.ts index f51b377..53940af 100644 --- a/orm/src/ModelsUnit.ts +++ b/orm/src/ModelsUnit.ts @@ -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, typeof Model> { protected base_path = './app/models' diff --git a/orm/src/builder/Builder.ts b/orm/src/builder/Builder.ts index f8bddac..dc1f2bd 100644 --- a/orm/src/builder/Builder.ts +++ b/orm/src/builder/Builder.ts @@ -1,5 +1,5 @@ -import {escape, EscapedValue, FieldSet, QuerySource} from './types.ts' -import { Select } from './type/Select.ts' +import {EscapedValue, FieldSet, QuerySource} from './types.ts' +import {Select} from './type/Select.ts' import RawValue from './RawValue.ts' import {Statement} from './Statement.ts' import {Update} from './type/Update.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 { // create table, alter table, drop table, select + /** + * Get a new SELECT statement. + * @param {...FieldSet} fields + * @return Select + */ public select(...fields: FieldSet[]): Select { fields = fields.flat() const select = new Select() return select.fields(...fields) } + /** + * Get a new UPDATE statement. + * @param {QuerySource} [target] + * @param {string} [alias] + * @return Update + */ public update(target?: QuerySource, alias?: string): Update { const update = new Update() 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 { const del = new Delete() 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 { const insert = new Insert() 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 { return new Statement(statement, interpolations) } + /** + * Get a new TRUNCATE statement. + * @param {QuerySource} [target] + * @param {string} [alias] + * @return Truncate + */ public truncate(target?: QuerySource, alias?: string): Truncate { return new Truncate(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') } diff --git a/orm/src/builder/RawValue.ts b/orm/src/builder/RawValue.ts index 24c4424..e69ce14 100644 --- a/orm/src/builder/RawValue.ts +++ b/orm/src/builder/RawValue.ts @@ -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 ) {} } diff --git a/orm/src/builder/Scope.ts b/orm/src/builder/Scope.ts index 9c2e351..1d2f1b9 100644 --- a/orm/src/builder/Scope.ts +++ b/orm/src/builder/Scope.ts @@ -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 } diff --git a/orm/src/builder/Statement.ts b/orm/src/builder/Statement.ts index 123d4fe..e142bca 100644 --- a/orm/src/builder/Statement.ts +++ b/orm/src/builder/Statement.ts @@ -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 extends ConnectionExecutable { constructor( + /** + * The statement to be executed. + * @type string + */ public statement: string, + /** + * The variables to be interpolated into the statement. + * @type Array + */ public interpolations: EscapedValue[] ) { super() diff --git a/orm/src/builder/scope/FunctionScope.ts b/orm/src/builder/scope/FunctionScope.ts index 151474b..66217ce 100644 --- a/orm/src/builder/scope/FunctionScope.ts +++ b/orm/src/builder/scope/FunctionScope.ts @@ -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() } diff --git a/orm/src/builder/type/ConnectionExecutable.ts b/orm/src/builder/type/ConnectionExecutable.ts index 1698048..e8d5463 100644 --- a/orm/src/builder/type/ConnectionExecutable.ts +++ b/orm/src/builder/type/ConnectionExecutable.ts @@ -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 { + /** + * 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 + */ async get_row(i: number): Promise { 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 { } } + /** + * Get a range of resultant rows for this query between the start and end indices. + * @param {string} start + * @param {string} end + * @return Promise + */ async get_range(start: number, end: number): Promise> { 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 { return inflated } + /** + * Get an iterator for this result set. + * @return ResultIterable + */ iterator(): ResultIterable { return new ResultIterable(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(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 + /** + * 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) { this.__target_operator = operator return this } + /** + * Execute the query and get back the raw result. + * @return Promise + */ async execute(): Promise { 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 { return this.execute_in_connection(this.__target_connection) } + /** + * Count the number of returned rows. + * @return Promise + */ async count(): Promise { 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 { return 0 } + /** + * True if the number of rows returned is greater than 0. + * @return Promise + */ async exists(): Promise { 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 + */ async execute_in_connection(connection: string | Connection): Promise { const conn = typeof connection === 'string' ? make(Database).connection(connection) : connection diff --git a/orm/src/builder/type/ConnectionMutable.ts b/orm/src/builder/type/ConnectionMutable.ts index 7c25b42..4b374e8 100644 --- a/orm/src/builder/type/ConnectionMutable.ts +++ b/orm/src/builder/type/ConnectionMutable.ts @@ -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 extends ConnectionExecutable { + /** + * The cached execution result. + * @type QueryResult + */ __execution_result?: QueryResult async get_row(i: number): Promise { @@ -28,6 +38,11 @@ export default abstract class ConnectionMutable 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 + */ async get_execution_result(): Promise { if ( this.__execution_result ) return this.__execution_result else return this.execute() diff --git a/orm/src/builder/type/Delete.ts b/orm/src/builder/type/Delete.ts index d4b1a39..c1674cd 100644 --- a/orm/src/builder/type/Delete.ts +++ b/orm/src/builder/type/Delete.ts @@ -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 extends ConnectionMutable { + /** + * The target table. + * @type QuerySource + */ protected _target?: QuerySource = undefined + + /** + * The where clauses. + * @type Array + */ protected _wheres: WhereStatement[] = [] + + /** + * The applied scopes. + * @type Array + */ protected _scopes: Scope[] = [] + + /** + * The fields to select. + * @type Array + */ protected _fields: string[] = [] + + /** + * Include the ONLY operator? + * @type boolean + */ protected _only: boolean = false sql(level = 0): string { @@ -29,17 +59,34 @@ export class Delete extends ConnectionMutable { ].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' ) { diff --git a/orm/src/builder/type/HavingBuilder.ts b/orm/src/builder/type/HavingBuilder.ts index 2cd3fe0..71332db 100644 --- a/orm/src/builder/type/HavingBuilder.ts +++ b/orm/src/builder/type/HavingBuilder.ts @@ -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 + */ protected _havings: HavingStatement[] = [] + /** + * Get the having clauses applied to the query. + * @type Array + */ 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, diff --git a/orm/src/builder/type/Insert.ts b/orm/src/builder/type/Insert.ts index 43006cd..0e5365e 100644 --- a/orm/src/builder/type/Insert.ts +++ b/orm/src/builder/type/Insert.ts @@ -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 extends ConnectionMutable { + /** + * The target table to insert into. + * @type QuerySource + */ protected _target?: QuerySource = undefined + + /** + * The columns to insert. + * @type Array + */ protected _columns: string[] = [] + + /** + * The row data to insert. + * @type Array + */ protected _rows: string[] = [] + + /** + * The fields to insert. + * @type Array + */ protected _fields: string[] = [] + + /** + * Return all data? + * @type boolean + */ protected _return_all = false sql(level = 0): string { @@ -36,17 +66,33 @@ export class Insert extends ConnectionMutable { ].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 extends ConnectionMutable { 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 extends ConnectionMutable { return this } + /** + * Add multiple field value objects to insert. + * @param {Array}rows + * @return Insert + */ rows(rows: FieldValueObject[]) { const [initial, ...rest] = rows @@ -96,6 +152,11 @@ export class Insert extends ConnectionMutable { 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' ) { diff --git a/orm/src/builder/type/Select.ts b/orm/src/builder/type/Select.ts index 020a35f..d578fd9 100644 --- a/orm/src/builder/type/Select.ts +++ b/orm/src/builder/type/Select.ts @@ -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 extends ConnectionExecutable { + /** + * The fields to select. + * @type Array + */ protected _fields: string[] = [] + + /** + * The source to select from. + * @type QuerySource + */ protected _source?: QuerySource = undefined + + /** + * Where clauses to apply. + * @type Array + */ protected _wheres: WhereStatement[] = [] + + /** + * The scopes to apply. + * @type Array + */ protected _scopes: Scope[] = [] + + /** + * Having clauses to apply. + * @type Array + */ 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 + */ protected _joins: Join[] = [] + + /** + * Include the DISTINCT operator? + * @type boolean + */ protected _distinct = false + + /** + * Group by clauses to apply. + * @type Array + */ protected _group_by: string[] = [] + + /** + * Order by clauses to apply. + * @type Array + */ protected _order: OrderStatement[] = [] + /** + * Include the DISTINCT operator. + * @return self + */ distinct() { this._distinct = true return this @@ -73,6 +154,12 @@ export class Select extends ConnectionExecutable { ].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, as?: string) { if ( field instanceof Select ) { this._fields.push(`${escape(field)}${as ? ' AS '+as : ''}`) @@ -83,11 +170,19 @@ export class Select extends ConnectionExecutable { return this } + /** + * Clear the selected fields. + * @return self + */ clear_fields() { this._fields = [] return this } + /** + * Get a copy of this query. + * @return Select + */ clone(): Select { const constructor = this.constructor as typeof Select if ( !isInstantiable>(constructor) ) { @@ -114,11 +209,21 @@ export class Select extends ConnectionExecutable { 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 extends ConnectionExecutable { 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 extends ConnectionExecutable { 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 extends ConnectionExecutable { 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 extends WhereBuilder, TableRefBuilder, HavingBuilder {} diff --git a/orm/src/builder/type/TableRefBuilder.ts b/orm/src/builder/type/TableRefBuilder.ts index 2c5eb2b..f446e56 100644 --- a/orm/src/builder/type/TableRefBuilder.ts +++ b/orm/src/builder/type/TableRefBuilder.ts @@ -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' ) { diff --git a/orm/src/builder/type/Truncate.ts b/orm/src/builder/type/Truncate.ts index 653302d..3472167 100644 --- a/orm/src/builder/type/Truncate.ts +++ b/orm/src/builder/type/Truncate.ts @@ -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 extends ConnectionMutable { + /** + * 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 extends ConnectionMutable { ].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 diff --git a/orm/src/builder/type/Update.ts b/orm/src/builder/type/Update.ts index f2a09dd..bb3325d 100644 --- a/orm/src/builder/type/Update.ts +++ b/orm/src/builder/type/Update.ts @@ -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 extends ConnectionMutable { + /** + * 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 + */ protected _sets: Collection = new Collection() + + /** + * Where clauses to be applied. + * @type Array + */ protected _wheres: WhereStatement[] = [] + + /** + * Scopes to be applied. + * @type Array + */ protected _scopes: Scope[] = [] + + /** + * Fields to update. + * @type Array + */ protected _fields: string[] = [] sql(level = 0): string { @@ -36,22 +70,44 @@ export class Update extends ConnectionMutable { ].filter(x => String(x).trim()).join(`\n${indent}`) } + /** + * Helper to serialize field value sets to raw SQL. + * @param {Collection} sets + * @param {number} level - the indentation level + * @return string + */ protected serialize_sets(sets: Collection, 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 extends ConnectionMutable { 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 extends ConnectionMutable { 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' ) { diff --git a/orm/src/builder/type/WhereBuilder.ts b/orm/src/builder/type/WhereBuilder.ts index 84c2230..25b51ad 100644 --- a/orm/src/builder/type/WhereBuilder.ts +++ b/orm/src/builder/type/WhereBuilder.ts @@ -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 + */ protected _wheres: WhereStatement[] = [] + + /** + * The scopes to be applied. + * @type Array + */ protected _scopes: Scope[] = [] + /** + * Get the where clauses applied to the query. + * @type Array + */ 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} 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} [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) } diff --git a/orm/src/builder/type/join/CrossJoin.ts b/orm/src/builder/type/join/CrossJoin.ts index 65dcbf9..e16d5a8 100644 --- a/orm/src/builder/type/join/CrossJoin.ts +++ b/orm/src/builder/type/join/CrossJoin.ts @@ -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' diff --git a/orm/src/builder/type/join/FullOuterJoin.ts b/orm/src/builder/type/join/FullOuterJoin.ts index 9b4cafe..87fa599 100644 --- a/orm/src/builder/type/join/FullOuterJoin.ts +++ b/orm/src/builder/type/join/FullOuterJoin.ts @@ -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' } diff --git a/orm/src/builder/type/join/InnerJoin.ts b/orm/src/builder/type/join/InnerJoin.ts index c9edc8f..813ae22 100644 --- a/orm/src/builder/type/join/InnerJoin.ts +++ b/orm/src/builder/type/join/InnerJoin.ts @@ -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' } diff --git a/orm/src/builder/type/join/Join.ts b/orm/src/builder/type/join/Join.ts index 6c665ec..c0a2035 100644 --- a/orm/src/builder/type/join/Join.ts +++ b/orm/src/builder/type/join/Join.ts @@ -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 + */ protected _wheres: WhereStatement[] = [] + + /** + * The scopes applied to this join. + * @type Array + */ 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 [ diff --git a/orm/src/builder/type/join/LeftJoin.ts b/orm/src/builder/type/join/LeftJoin.ts index f65f0d0..3ccdffb 100644 --- a/orm/src/builder/type/join/LeftJoin.ts +++ b/orm/src/builder/type/join/LeftJoin.ts @@ -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' } diff --git a/orm/src/builder/type/join/LeftOuterJoin.ts b/orm/src/builder/type/join/LeftOuterJoin.ts index 65d6fbd..ab5737f 100644 --- a/orm/src/builder/type/join/LeftOuterJoin.ts +++ b/orm/src/builder/type/join/LeftOuterJoin.ts @@ -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' } diff --git a/orm/src/builder/type/join/RightJoin.ts b/orm/src/builder/type/join/RightJoin.ts index 9845db7..37305af 100644 --- a/orm/src/builder/type/join/RightJoin.ts +++ b/orm/src/builder/type/join/RightJoin.ts @@ -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' } diff --git a/orm/src/builder/type/join/RightOuterJoin.ts b/orm/src/builder/type/join/RightOuterJoin.ts index fa51c9c..0faf32a 100644 --- a/orm/src/builder/type/join/RightOuterJoin.ts +++ b/orm/src/builder/type/join/RightOuterJoin.ts @@ -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' } diff --git a/orm/src/builder/type/result/ObjectResultOperator.ts b/orm/src/builder/type/result/ObjectResultOperator.ts index e8b2ae4..4f35ab5 100644 --- a/orm/src/builder/type/result/ObjectResultOperator.ts +++ b/orm/src/builder/type/result/ObjectResultOperator.ts @@ -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 { inflate_row(row: QueryRow): QueryRow { diff --git a/orm/src/builder/type/result/ResultCollection.ts b/orm/src/builder/type/result/ResultCollection.ts index 16919d4..504c2d2 100644 --- a/orm/src/builder/type/result/ResultCollection.ts +++ b/orm/src/builder/type/result/ResultCollection.ts @@ -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 extends AsyncCollection { constructor( executable: ResultIterable, diff --git a/orm/src/builder/type/result/ResultIterable.ts b/orm/src/builder/type/result/ResultIterable.ts index 9d7e07c..ad2dc8f 100644 --- a/orm/src/builder/type/result/ResultIterable.ts +++ b/orm/src/builder/type/result/ResultIterable.ts @@ -1,11 +1,18 @@ import {Iterable} from '../../../../../lib/src/collection/Iterable.ts' import ConnectionExecutable from '../ConnectionExecutable.ts' import {Collection} from '../../../../../lib/src/collection/Collection.ts' -import {QueryRow} from '../../../db/types.ts' +/** + * An Iterable implementation which retrieves results from a database query. + * @extends Iterable + */ export class ResultIterable extends Iterable { constructor( + /** + * The executable database query to base the iterable on. + * @type ConnectionExecutable + */ protected executable: ConnectionExecutable ) { super() } diff --git a/orm/src/builder/type/result/ResultOperator.ts b/orm/src/builder/type/result/ResultOperator.ts index 3b7d642..761f229 100644 --- a/orm/src/builder/type/result/ResultOperator.ts +++ b/orm/src/builder/type/result/ResultOperator.ts @@ -1,11 +1,35 @@ import {QueryRow} from '../../../db/types.ts' import ConnectionExecutable from '../ConnectionExecutable.ts' -import {Model} from '../../../model/Model.ts' import {Collection} from '../../../../../lib/src/collection/Collection.ts' +/** + * Base class for query result operators which process query rows into other + * formats after retrieval. + * @abstract + */ export default abstract class ResultOperator { + /** + * Called in bulk before result rows are inflated. This can be used to bulk-preload + * additional data that might be added into the dataset. + * + * For example, the ModelResultOperator uses this to eager load specified relations. + * + * @param {ConnectionExecutable} query + * @param {Collection} results + * @return Promise + */ public async process_eager_loads(query: ConnectionExecutable, results: Collection): Promise { } + /** + * Convert a row from the raw query result to the target format. + * @param {QueryRow} row + */ abstract inflate_row(row: QueryRow): T + + /** + * Convert the target format back to a raw query result. + * @param item + * @return QueryRow + */ abstract deflate_row(item: T): QueryRow } diff --git a/orm/src/builder/type/result/ResultSet.ts b/orm/src/builder/type/result/ResultSet.ts index da8017c..41f807c 100644 --- a/orm/src/builder/type/result/ResultSet.ts +++ b/orm/src/builder/type/result/ResultSet.ts @@ -1,22 +1,47 @@ import { Iterable } from '../../../../../lib/src/collection/Iterable.ts' -import ConnectionExecutable from "../ConnectionExecutable.ts"; -import {QueryRow} from "../../../db/types.ts"; -import {Collection} from "../../../../../lib/src/collection/Collection.ts"; +import ConnectionExecutable from '../ConnectionExecutable.ts' +import {QueryRow} from '../../../db/types.ts' +import {Collection} from '../../../../../lib/src/collection/Collection.ts' +/** + * Abstract iterable that wraps an executable query. + * @extends Iterable + * @abstract + */ export abstract class ResultSet extends Iterable { protected constructor( + /** + * The executable query to wrap. + * @type ConnectionExecutable + */ protected executeable: ConnectionExecutable, ) { super() } + /** + * Process a single incoming query row to the output format. + * @param {QueryRow} row + * @return Promise + */ abstract async process_row(row: QueryRow): Promise + /** + * Get the result at index i. + * @param {number} i + * @return Promise + */ async at_index(i: number) { return this.process_row(await this.executeable.get_row(i)) } + /** + * Get a collection of results for the given range of rows. + * @param {number} start + * @param {number} end + * @return Promise + */ async from_range(start: number, end: number) { const results = await this.executeable.get_range(start, end) const returns = new Collection() @@ -27,6 +52,10 @@ export abstract class ResultSet extends Iterable { return returns } + /** + * Count the number of results. + * @return Promise + */ async count() { return this.executeable.count() } diff --git a/orm/src/builder/types.ts b/orm/src/builder/types.ts index cc15193..e96e8a6 100644 --- a/orm/src/builder/types.ts +++ b/orm/src/builder/types.ts @@ -1,37 +1,111 @@ import {WhereOperator} from '../../../lib/src/collection/Where.ts' import RawValue from './RawValue.ts' -import {Select} from "./type/Select.ts"; +import {Select} from './type/Select.ts' +/** + * Represents a field or set of fields. + */ export type FieldSet = string | string[] + +/** + * Represents a table name, or table name and alias. + */ export type QuerySource = string | { ref: QuerySource, alias: string } +/** + * Valid JOIN clause operators. + */ export type JoinOperator = 'JOIN' | 'LEFT JOIN' | 'LEFT OUTER JOIN' | 'RIGHT JOIN' | 'RIGHT OUTER JOIN' | 'FULL OUTER JOIN' | 'INNER JOIN' | 'CROSS JOIN' +/** + * Valid operators which can join WHERE clauses. + */ export type WherePreOperator = 'AND' | 'OR' | 'AND NOT' | 'OR NOT' + +/** + * Abstract representation of a single WHERE clause. + */ export type WhereClause = { field: string, operator: SQLWhereOperator, operand: string, preop: WherePreOperator } + +/** + * Group of where clauses, and the operator which should join them. + */ export type WhereGroup = { items: WhereStatement[], preop: WherePreOperator } + +/** + * A single WHERE statement. + */ export type WhereStatement = WhereClause | WhereGroup + +/** + * Operators which can be used in SQL WHERE clauses. + */ export type SQLWhereOperator = WhereOperator | 'IN' | 'NOT IN' | 'LIKE' | 'BETWEEN' | 'NOT BETWEEN' | 'IS' | 'IS NOT' + +/** + * Directions for ORDER BY clauses. + */ export type OrderDirection = 'ASC' | 'DESC' + +/** + * Abstract representation of an ORDER BY clause. + */ export type OrderStatement = { direction: OrderDirection, field: string } +/** + * Valid operators which can join HAVING clauses. + */ export type HavingPreOperator = WherePreOperator + +/** + * Abstract representation of a single HAVING clause. + */ export type HavingClause = WhereClause + +/** + * Group of having clauses, and the operator which should join them. + */ export type HavingGroup = WhereGroup + +/** + * A single HAVING statement. + */ export type HavingStatement = HavingClause | HavingGroup + +/** + * Valid operators which can be used in SQL HAVING clauses. + */ export type SQLHavingOperator = SQLWhereOperator +/** + * A value which can be escaped to be interpolated into an SQL query. + */ export type EscapedValue = string | number | boolean | Date | RawValue | EscapedValue[] | Select +/** + * Representation of a field and its value. + */ export type FieldValue = { field: string, value: EscapedValue } + +/** + * Object representation of a number of fields and their values. + */ export type FieldValueObject = { [field: string]: EscapedValue } +/** + * Abstract reference to a particular database table, and its alias. + */ export type TableRef = { table: string, database?: string, alias?: string } +/** + * Returns true if the given object is a valid table ref. + * @param something + * @return boolean + */ export function isTableRef(something: any): something is TableRef { let is = true is = is && typeof something?.table === 'string' @@ -47,14 +121,29 @@ export function isTableRef(something: any): something is TableRef { return is } +/** + * Returns true if the given item is a valid WHERE pre-operator. + * @param something + * @return boolean + */ export function isWherePreOperator(something: any): something is WherePreOperator { return ['AND', 'OR', 'AND NOT', 'OR NOT'].includes(something) } +/** + * Returns true if the given item is a valid HAVING clause. + * @param something + * @return boolean + */ export function isHavingClause(something: any): something is HavingClause { return isWhereClause(something) } +/** + * Returns true if the given item is a valid WHERE clause. + * @param something + * @return boolean + */ export function isWhereClause(something: any): something is WhereClause { return typeof something?.field === 'string' && typeof something?.operator === 'string' // TODO check this better @@ -62,20 +151,40 @@ export function isWhereClause(something: any): something is WhereClause { && isWherePreOperator(something?.preop) } +/** + * Returns true if the given item is a valid HAVING group. + * @param something + * @return boolean + */ export function isHavingGroup(something: any): something is HavingGroup { return isWhereGroup(something) } +/** + * Returns true if the given item is a valid WHERE group. + * @param something + * @return boolean + */ export function isWhereGroup(something: any): something is WhereGroup { return Array.isArray(something?.items) && something.items.every((item: any) => isWhereStatement(item)) && isWherePreOperator(something?.preop) } +/** + * Returns true if the given value is a valid where statement. + * @param something + * @return boolean + */ export function isWhereStatement(something: any): something is WhereStatement { return isWhereClause(something) || isWhereGroup(something) } +/** + * Escapes the value so it can be inserted into an SQL query string. + * @param {EscapedValue} value + * @return string + */ export function escape(value: EscapedValue): string { if ( value instanceof Select ) { return `(${value.sql(5)})` diff --git a/orm/src/db/Connection.ts b/orm/src/db/Connection.ts index 05db17f..457b7ad 100644 --- a/orm/src/db/Connection.ts +++ b/orm/src/db/Connection.ts @@ -1,20 +1,50 @@ import {QueryResult} from './types.ts' +/** + * Error thrown when a connection is used before it is ready. + * @extends Error + */ export class ConnectionNotReadyError extends Error { constructor(name = '') { super(`The connection ${name} is not ready and cannot execute queries.`) } } +/** + * Abstract base class for database connections. + * @abstract + */ export abstract class Connection { constructor( + /** + * The name of this connection + * @type string + */ public readonly name: string, + /** + * This connection's config object + */ public readonly config: any = {}, ) {} + /** + * Open the connection. + * @return Promise + */ public abstract async init(): Promise + + /** + * Execute an SQL query and get the result. + * @param {string} query + * @return Promise + */ public abstract async query(query: string): Promise // TODO query result + + /** + * Close the connection. + * @return Promise + */ public abstract async close(): Promise } diff --git a/orm/src/db/PostgresConnection.ts b/orm/src/db/PostgresConnection.ts index 9911fe0..ab34fde 100644 --- a/orm/src/db/PostgresConnection.ts +++ b/orm/src/db/PostgresConnection.ts @@ -4,7 +4,15 @@ import {collect, Collection} from '../../../lib/src/collection/Collection.ts' import { QueryResult, QueryRow } from './types.ts' import { logger } from '../../../lib/src/service/logging/global.ts' +/** + * Database connection class for PostgreSQL connections. + * @extends Connection + */ export default class PostgresConnection extends Connection { + /** + * The underlying PostgreSQL client. + * @type Client + */ private _client?: Client public async init() { diff --git a/orm/src/db/types.ts b/orm/src/db/types.ts index e5a649f..3c45604 100644 --- a/orm/src/db/types.ts +++ b/orm/src/db/types.ts @@ -1,13 +1,26 @@ import { Collection } from '../../../lib/src/collection/Collection.ts' +/** + * A single query row, as an object. + */ export type QueryRow = { [key: string]: any } + +/** + * A valid key on a model. + */ export type ModelKey = string | number +/** + * Interface for the result of a query execution. + */ export interface QueryResult { rows: Collection, row_count: number, } +/** + * Database column types. + */ export enum Type { bigint = 'bigint', int8 = 'bigint', diff --git a/orm/src/error/NoTargetOperatorError.ts b/orm/src/error/NoTargetOperatorError.ts index 1c7ea3e..108f494 100644 --- a/orm/src/error/NoTargetOperatorError.ts +++ b/orm/src/error/NoTargetOperatorError.ts @@ -1,3 +1,7 @@ +/** + * Error thrown when a query is executed but there is no result operator set. + * @extends Error + */ export default class NoTargetOperatorError extends Error { constructor(msg = 'This query has no defined target operator.') { super(msg) diff --git a/orm/src/model/Field.ts b/orm/src/model/Field.ts index 31afc77..c0117b1 100644 --- a/orm/src/model/Field.ts +++ b/orm/src/model/Field.ts @@ -2,10 +2,14 @@ import { Reflect } from '../../../lib/src/external/reflect.ts' import { Collection } from '../../../lib/src/collection/Collection.ts' import { logger } from '../../../lib/src/service/logging/global.ts' import {Type} from '../db/types.ts' -import {Model} from "./Model.ts"; export const DATON_ORM_MODEL_FIELDS_METADATA_KEY = 'daton:orm:modelFields.ts' +/** + * Get the model field metadata from a model class. + * @param model + * @return Collection + */ export function get_fields_meta(model: any): Collection { const fields = Reflect.getMetadata(DATON_ORM_MODEL_FIELDS_METADATA_KEY, model.constructor) if ( !(fields instanceof Collection) ) { @@ -15,16 +19,31 @@ export function get_fields_meta(model: any): Collection { return fields as Collection } +/** + * Set the model field metadata for a model class. + * @param model + * @param {Collection} fields + */ export function set_model_fields_meta(model: any, fields: Collection) { Reflect.defineMetadata(DATON_ORM_MODEL_FIELDS_METADATA_KEY, fields, model.constructor) } +/** + * Abstract representation of a field on a model. + */ export interface ModelField { database_key: string, model_key: string | symbol, type: any, } +/** + * Property decorator for a field on a model. If no column name is provided, the database column name + * will be the same as the property name on the model. + * @param {Type} type - the column type + * @param {string} [database_key] - the database column name + * @constructor + */ export function Field(type: Type, database_key?: string): PropertyDecorator { return (target, model_key) => { if ( !database_key ) database_key = String(model_key) diff --git a/orm/src/model/Model.ts b/orm/src/model/Model.ts index 448f4ce..edb8a29 100644 --- a/orm/src/model/Model.ts +++ b/orm/src/model/Model.ts @@ -22,6 +22,9 @@ import {HasOneOrMany} from './relation/HasOneOrMany.ts' // TODO separate read/write connections // TODO manual dirty flags +/** + * Dehydrated state of this model. + */ export type ModelJSONState = { key_name: string, key?: string | number, @@ -29,6 +32,9 @@ export type ModelJSONState = { ephemeral_values?: string, } +/** + * Cached relation value. + */ export type CachedRelation = { accessor_name: string | symbol, relation: HasOne, // TODO generalize @@ -151,6 +157,10 @@ abstract class Model> extends Builder implements Rehydrata */ protected deleted$ = new BehaviorSubject>() + /** + * Cached relation values. + * @type Collection + */ public readonly relation_cache = new Collection() /** @@ -1012,6 +1022,11 @@ abstract class Model> extends Builder implements Rehydrata return new HasMany(this as any, related_inst, relation.local_key, relation.foreign_key) } + /** + * Get the relation instance for a given property name. + * @param {string} name + * @return Relation + */ public get_relation>(name: string): Relation { // @ts-ignore const rel: any = this[name]() diff --git a/orm/src/model/ModelResultOperator.ts b/orm/src/model/ModelResultOperator.ts index bf8d23f..8e7647b 100644 --- a/orm/src/model/ModelResultOperator.ts +++ b/orm/src/model/ModelResultOperator.ts @@ -8,9 +8,17 @@ import ConnectionExecutable from '../builder/type/ConnectionExecutable.ts' import {Collection} from '../../../lib/src/collection/Collection.ts' import {ModelSelect} from './query/ModelSelect.ts' +/** + * Database result operator that instantiates models for each row. + * @extends ResultOperator + */ @Injectable() export default class ModelResultOperator> extends ResultOperator { constructor( + /** + * The model class to load rows into. + * @type Instantiable + */ protected ModelClass: Instantiable, ) { super() diff --git a/orm/src/model/RelationResultOperator.ts b/orm/src/model/RelationResultOperator.ts deleted file mode 100644 index f6907a2..0000000 --- a/orm/src/model/RelationResultOperator.ts +++ /dev/null @@ -1,19 +0,0 @@ -import ModelResultOperator from "./ModelResultOperator.ts"; -import {Model} from "./Model.ts"; -import Instantiable from "../../../di/src/type/Instantiable.ts"; -import {HasOne} from "./relation/HasOne.ts"; -import {QueryRow} from "../db/types.ts"; - -export default class RelationResultOperator> extends ModelResultOperator { - constructor( - protected ModelClass: Instantiable, - protected relation: HasOne, - ) { - super(ModelClass) - } - - inflate_row(row: QueryRow): T { - const instance = super.inflate_row(row) - return instance - } -} diff --git a/orm/src/model/filter.ts b/orm/src/model/filter.ts index 62dc55b..5a15573 100644 --- a/orm/src/model/filter.ts +++ b/orm/src/model/filter.ts @@ -1,7 +1,9 @@ import {WhereBuilder} from '../builder/type/WhereBuilder.ts' import {logger} from '../../../lib/src/service/logging/global.ts' -import {EscapedValue} from '../builder/types.ts' +/** + * Operators for filter objects. + */ export enum FilterOp { eq = '$eq', in = '$in', @@ -11,8 +13,17 @@ export enum FilterOp { gte = '$gte', } +/** + * An object-like query filter. + */ export type QueryFilter = { [key: string]: any } +/** + * Given an object-like query filter, apply it to the database WHERE clause. + * @param {QueryFilter} filter + * @param {WhereBuilder} where + * @return WhereBuilder + */ export function apply_filter_to_where(filter: QueryFilter, where: WhereBuilder): WhereBuilder { for ( const field in filter ) { if ( !filter.hasOwnProperty(field) ) continue diff --git a/orm/src/model/query/ModelSelect.ts b/orm/src/model/query/ModelSelect.ts index 98f1c21..895dbdb 100644 --- a/orm/src/model/query/ModelSelect.ts +++ b/orm/src/model/query/ModelSelect.ts @@ -1,17 +1,40 @@ import {Select} from '../../builder/type/Select.ts' +/** + * Query builder SELECT clause, with added features for working with models. + * @extends Select + */ export class ModelSelect extends Select { + /** + * List of relations to eager load. + * @type Array + */ protected _withs: string[] = [] + /** + * Eager load a relation. + * @example posts + * @example posts.comments + * @param {string} related + * @return self + */ public with(related: string) { this._withs.push(related) return this } + /** + * Get the relations to eager load. + * @type Array + */ public get eager_relations(): string[] { return [...this._withs] } + /** + * Make a copy of this query.e + * @return ModelSelect + */ clone(): ModelSelect { const select = super.clone() as ModelSelect select._withs = this._withs diff --git a/orm/src/model/relation/HasMany.ts b/orm/src/model/relation/HasMany.ts index 560c9c7..2e18707 100644 --- a/orm/src/model/relation/HasMany.ts +++ b/orm/src/model/relation/HasMany.ts @@ -2,8 +2,21 @@ import {Model} from '../Model.ts' import {HasOneOrMany} from './HasOneOrMany.ts' import {Collection} from '../../../../lib/src/collection/Collection.ts' +/** + * Relation class for one-to-many relations. + * @extends HasOneOrMany + */ export class HasMany, T2 extends Model> extends HasOneOrMany { + /** + * The cached value of this relation. + * @type Collection + */ protected _value?: Collection + + /** + * True if this relation has been loaded. + * @type boolean + */ protected _loaded = false public async get(): Promise> { diff --git a/orm/src/model/relation/HasOne.ts b/orm/src/model/relation/HasOne.ts index b86d44b..e255c08 100644 --- a/orm/src/model/relation/HasOne.ts +++ b/orm/src/model/relation/HasOne.ts @@ -1,10 +1,23 @@ import {Model} from '../Model.ts' import {HasOneOrMany} from './HasOneOrMany.ts' -import {Collection} from "../../../../lib/src/collection/Collection.ts"; -import {Logging} from "../../../../lib/src/service/logging/Logging.ts"; +import {Collection} from '../../../../lib/src/collection/Collection.ts' +import {Logging} from '../../../../lib/src/service/logging/Logging.ts' +/** + * Relation class for one-to-one relations. + * @extends HasOneOrMany + */ export class HasOne, T2 extends Model> extends HasOneOrMany { + /** + * The cached value of this relation. + * @type Model + */ protected _value?: T2 + + /** + * True if the relation has been loaded. + * @type boolean + */ protected _loaded = false public async get(): Promise { diff --git a/orm/src/model/relation/HasOneOrMany.ts b/orm/src/model/relation/HasOneOrMany.ts index b281615..a871e6d 100644 --- a/orm/src/model/relation/HasOneOrMany.ts +++ b/orm/src/model/relation/HasOneOrMany.ts @@ -5,26 +5,63 @@ import {WhereBuilder} from '../../builder/type/WhereBuilder.ts' import {ModelSelect} from '../query/ModelSelect.ts' import {Collection} from '../../../../lib/src/collection/Collection.ts' +/** + * Abstract relation class for one-to-one and one-to-many relations. + * @extends Relation + * @abstract + */ export abstract class HasOneOrMany, T2 extends Model> extends Relation { constructor( + /** + * The parent model. + * @type Model + */ protected parent: T, + /** + * The model which is related. + * @type Model + */ public readonly related: T2, + /** + * The key on the related model. + * @type string + */ protected foreign_key_spec?: string, + /** + * The key on the parent model. + * @type string + */ protected local_key_spec?: string, ) { super(parent, related) } + /** + * Get the key to match on the related model. + * @type string + */ public get foreign_key() { return this.foreign_key_spec || this.parent.key_name() } + /** + * Get the key to match on the parent model. + * @type string + */ public get local_key() { return this.local_key_spec || this.foreign_key } + /** + * Get the table-qualified key to match on the related model. + * @type string + */ public get qualified_foreign_key() { return this.related.qualify_column(this.foreign_key) } + /** + * Get the table-qualified key to match on the parent model. + * @type string + */ public get qualified_local_key() { return this.related.qualify_column(this.local_key) } diff --git a/orm/src/model/relation/Relation.ts b/orm/src/model/relation/Relation.ts index 7b54cff..7c9bdc9 100644 --- a/orm/src/model/relation/Relation.ts +++ b/orm/src/model/relation/Relation.ts @@ -1,6 +1,5 @@ import {Model} from '../Model.ts' import AppClass from '../../../../lib/src/lifecycle/AppClass.ts' -import RelationResultOperator from '../RelationResultOperator.ts' import ConnectionExecutable from '../../builder/type/ConnectionExecutable.ts' import {AsyncCollection} from '../../../../lib/src/collection/AsyncCollection.ts' import {Collection} from '../../../../lib/src/collection/Collection.ts' @@ -13,41 +12,116 @@ import {Delete} from '../../builder/type/Delete.ts' import ModelResultOperator from '../ModelResultOperator.ts' import {ModelSelect} from '../query/ModelSelect.ts' +/** + * The result of loading a relation. + */ export type RelationResult = T | Collection | undefined +/** + * Abstract base class for model-to-model relations. + * @extends AppClass + * @abstract + */ export abstract class Relation, T2 extends Model> extends AppClass { constructor( + /** + * The parent model. + * @type Model + */ protected parent: T, + /** + * The related model. + * @type Model + */ public readonly related: T2, ) { super() } + /** + * Get the value on the parent model that is used in the relation. + */ protected abstract get parent_value(): any + /** + * Get the result operator for this relation. + * @return ModelResultOperator + */ public get_operator() { const related_class = this.related.constructor as typeof Model return this.make(ModelResultOperator, related_class) } + /** + * Get the query instance for this relation. + * @returns ConnectionExecutable + * @abstract + */ public abstract query(): ConnectionExecutable + /** + * Scope an incoming query to the result set of this relation. + * @param {WhereBuilder} where + * @abstract + */ public abstract scope_query(where: WhereBuilder): void + /** + * Build a query to eager load this relation. + * @param {ModelSelect} parent_query - the incoming query + * @param {Collection} result - the loaded parents + * @return ModelSelect + * @abstract + */ public abstract build_eager_query(parent_query: ModelSelect, result: Collection): ModelSelect + /** + * Match the results from an eager load to only those belonging to this relation's parent. + * @param {Collection} possibly_related + * @return Collection + * @abstract + */ public abstract match_results(possibly_related: Collection): Collection + /** + * Set the value of this relation, caching it. + * @param {Collection} related + * @abstract + */ public abstract set_value(related: Collection): void + + /** + * Get the cached value of this relation. + * @return RelationResult + * @abstract + */ public abstract get_value(): Collection | T2 | undefined + + /** + * Returns true if this relation has been loaded. + * @return boolean + * @abstract + */ public abstract is_loaded(): boolean + /** + * Fetch the results of this relation, by query. + * @return AsyncCollection + */ public fetch(): AsyncCollection { return this.query().results() } + /** + * Get the results of this relation from the database. + * @return Promise + */ public abstract get(): Promise> + /** + * Allow awaiting the relation to get the cached results, or fetched. + * @param callback + */ public then(callback: (result: RelationResult) => any) { if ( this.is_loaded() ) { callback(this.get_value() as RelationResult) @@ -62,28 +136,53 @@ export abstract class Relation, T2 extends Model> extends } } + /** + * Get the current value of the relation. + * @type RelationResult + */ public get value(): RelationResult { return this.get_value() } + /** + * Get the query source of the related model. + * @type QuerySource + */ public get related_query_source() { const related_class = this.related.constructor as typeof Model return related_class.query_source() } + /** + * Get an instance of the relation builder for this model. + * @return RelationBuilder + */ public builder(): RelationBuilder { return new RelationBuilder(this) } + /** + * Get a select statement for this relation. + * @param {...FieldSet} fields + * @return Select + */ public select(...fields: FieldSet[]): Select { if ( fields.length < 1 ) fields.push(this.related.qualify_column('*')) return this.builder().select(...fields) } + /** + * Get an update statement for this relation. + * @return Update + */ public update(): Update { return this.builder().update() } + /** + * Get a delete statement for this relation. + * @return Delete + */ public delete(): Delete { return this.builder().delete() } diff --git a/orm/src/model/relation/RelationBuilder.ts b/orm/src/model/relation/RelationBuilder.ts index b1312b0..5f3daad 100644 --- a/orm/src/model/relation/RelationBuilder.ts +++ b/orm/src/model/relation/RelationBuilder.ts @@ -6,8 +6,16 @@ import {FieldSet, QuerySource} from '../../builder/types.ts' import {Delete} from '../../builder/type/Delete.ts' import {Model} from '../Model.ts' +/** + * Query builder scoped to relation values. + * @extends Builder + */ export class RelationBuilder> extends Builder { constructor( + /** + * The relation whose related model should be the subject of these queries. + * @type Relation + */ protected relation: Relation ) { super() diff --git a/orm/src/model/relation/decorators.ts b/orm/src/model/relation/decorators.ts index 0a20a13..6a82d02 100644 --- a/orm/src/model/relation/decorators.ts +++ b/orm/src/model/relation/decorators.ts @@ -1,5 +1,9 @@ import {Model} from '../Model.ts' +/** + * Decorator for model relations. This caches the relation value, so lookups are only done once. + * @constructor + */ export function Relation(): MethodDecorator { return (target: any, propertyKey, descriptor) => { console.log('relation decorator', target, propertyKey, descriptor) diff --git a/orm/src/service/Database.ts b/orm/src/service/Database.ts index 0d42468..c67318c 100644 --- a/orm/src/service/Database.ts +++ b/orm/src/service/Database.ts @@ -2,22 +2,43 @@ import { Service } from '../../../di/src/decorator/Service.ts' import { Connection } from '../db/Connection.ts' import PostgresConnection from '../db/PostgresConnection.ts' +/** + * Error thrown if two connections with the same name are registered. + * @extends Error + */ export class DuplicateConnectionNameError extends Error { constructor(connection_name: string) { super(`A database connection with the name "${connection_name}" already exists.`) } } +/** + * Error thrown if a connection name is accessed when there is no corresponding connection registered. + * @extends Error + */ export class NoSuchDatabaseConnectionError extends Error { constructor(connection_name: string) { super(`No database connection exists with the name: "${connection_name}"`) } } +/** + * Service that manages and creates database connections. + */ @Service() export default class Database { + /** + * The created connections, keyed by name. + * @type object + */ private connections: { [name: string]: Connection } = {} + /** + * Create a new PostgreSQL connection. + * @param {string} name + * @param {object} config + * @return Promise + */ async postgres(name: string, config: { [key: string]: any }): Promise { if ( this.connections[name] ) throw new DuplicateConnectionNameError(name) @@ -28,6 +49,11 @@ export default class Database { return conn } + /** + * Get a connection by name. + * @param name + * @return Connection + */ connection(name: string): Connection { if ( !this.connections[name] ) throw new NoSuchDatabaseConnectionError(name)