support route handler arguments; add daton::static file server

This commit is contained in:
Garrett Mills 2020-09-05 08:58:34 -05:00
parent 27ee1a552b
commit 601649e699
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246
18 changed files with 231 additions and 32 deletions

View File

@ -1,14 +1,12 @@
static assets TLS support
middleware
internationalization internationalization
uploads & universal path uploads & universal path
CLI - view routes, template generation, start server, directives, output, args CLI - view routes, template generation, start server, directives, output, args
request level error handler nicer error and home pages
develop/prod/debug modes
favicon favicon
utility - root, path, is_windows, is_linux, is_mac utility - is_windows, is_linux, is_mac
authentication - user/session, oauth, jwt, &c. authentication - user/session, oauth, jwt, &c.
orm relations, subqueries, enum/bit fields, json handlers, scopes orm enum/bit fields, json handlers, scopes
redis - redis client, redis rehydrated classes, redis sessions redis - redis client, redis rehydrated classes, redis sessions
less/scss less/scss
notifications - gotify/push/other mechanisms notifications - gotify/push/other mechanisms

2
app/assets/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -9,3 +9,4 @@ export { default as HttpServerUnit } from '../../lib/src/unit/HttpServer.ts'
export { default as RoutingUnit } from '../../lib/src/unit/Routing.ts' export { default as RoutingUnit } from '../../lib/src/unit/Routing.ts'
export { default as ServicesUnit } from '../../lib/src/unit/Services.ts' export { default as ServicesUnit } from '../../lib/src/unit/Services.ts'
export { default as ViewEngineUnit } from '../../lib/src/unit/ViewEngine.ts' export { default as ViewEngineUnit } from '../../lib/src/unit/ViewEngine.ts'
export { default as DatonMiddlewareUnit } from '../../lib/src/unit/DatonMiddleware.ts'

View File

@ -14,5 +14,5 @@ export default {
session: { session: {
driver: 'database', // memory | database driver: 'database', // memory | database
model: 'http:Session', // required for database model: 'http:Session', // required for database
} },
} }

View File

@ -6,5 +6,6 @@ export default {
get: { get: {
'/': 'controller::Home.get_home', '/': 'controller::Home.get_home',
'/maybe': ['middleware::Test', 'controller::Home.get_home'], '/maybe': ['middleware::Test', 'controller::Home.get_home'],
'/statics/**': { handler: 'daton::static', arg: 'assets' },
}, },
} as RouterDefinition } as RouterDefinition

View File

@ -10,6 +10,7 @@ import {
RoutingUnit, RoutingUnit,
ServicesUnit, ServicesUnit,
ViewEngineUnit, ViewEngineUnit,
DatonMiddlewareUnit,
} from './bundle/daton_units.ts' } from './bundle/daton_units.ts'
export default [ export default [
@ -19,6 +20,7 @@ export default [
ModelsUnit, ModelsUnit,
ViewEngineUnit, ViewEngineUnit,
HttpKernelUnit, HttpKernelUnit,
DatonMiddlewareUnit,
MiddlewareUnit, MiddlewareUnit,
ControllerUnit, ControllerUnit,
RoutesUnit, RoutesUnit,

View File

@ -1,4 +1,4 @@
// export * from 'https://deno.land/x/postgres@v0.4.3/mod.ts' // export * from 'https://deno.land/x/postgres@v0.4.4/mod.ts'
// FIXME: waiting on https://github.com/deno-postgres/deno-postgres/pull/166 // FIXME: waiting on https://github.com/deno-postgres/deno-postgres/pull/166
export * from 'https://raw.githubusercontent.com/glmdev/deno-postgres/master/mod.ts' export * from 'https://raw.githubusercontent.com/glmdev/deno-postgres/master/mod.ts'

View File

@ -3,4 +3,5 @@ export { config as dotenv } from 'https://deno.land/x/dotenv/mod.ts'
export * as path from 'https://deno.land/std@0.67.0/path/mod.ts' export * as path from 'https://deno.land/std@0.67.0/path/mod.ts'
export * as fs from 'https://deno.land/std@0.67.0/fs/mod.ts' export * as fs from 'https://deno.land/std@0.67.0/fs/mod.ts'
export { generate as uuid } from 'https://deno.land/std@0.67.0/uuid/v4.ts' export { generate as uuid } from 'https://deno.land/std@0.67.0/uuid/v4.ts'
export * as file_server from 'https://deno.land/std@0.67.0/http/file_server.ts'
// export { moment } from 'https://deno.land/x/moment/moment.ts' // export { moment } from 'https://deno.land/x/moment/moment.ts'

View File

@ -1,6 +1,7 @@
import {HTTPResponse} from './type/HTTPResponse.ts' import {HTTPResponse} from './type/HTTPResponse.ts'
import {HTTPRequest} from './type/HTTPRequest.ts' import {HTTPRequest} from './type/HTTPRequest.ts'
import {ServerRequest} from '../external/http.ts' import {ServerRequest} from '../external/http.ts'
import {file_server} from '../external/std.ts'
import {CookieJar} from './CookieJar.ts' import {CookieJar} from './CookieJar.ts'
/** /**

View File

@ -49,10 +49,10 @@ export default class ApplyRouteHandlers extends Module {
for ( const handler of request.route.handlers ) { for ( const handler of request.route.handlers ) {
try { try {
let result let result
if ( isRouteHandlerClass(handler) ) { if ( isRouteHandlerClass(handler.handler) ) {
result = await handler.handleRequest(current_request) result = await handler.handler.handleRequest(current_request, handler.arg)
} else { } else {
result = await handler(current_request) result = await handler.handler(current_request, handler.arg)
} }
if ( result instanceof Request ) { if ( result instanceof Request ) {

View File

@ -0,0 +1,45 @@
import ResponseFactory from './ResponseFactory.ts'
import {Request} from '../Request.ts'
import {file_server} from '../../external/std.ts'
import {HTTPStatus} from '../../const/http.ts'
import {Logging} from '../../service/logging/Logging.ts'
import {Injectable} from '../../../../di/src/decorator/Injection.ts'
/**
* Response factory to send the contents of a file given its path.
* @extends ResponseFactory
*/
@Injectable()
export default class FileResponseFactory extends ResponseFactory {
constructor(
protected readonly logger: Logging,
/**
* The path to the file to be sent.
* @type string
*/
public readonly path: string,
) { super() }
/**
* Write this response factory to the given request's response.
* @param {Request} request
* @return Request
*/
public async write(request: Request): Promise<Request> {
request = await super.write(request)
const content = await file_server.serveFile(request.to_native, this.path)
const length = content.headers && content.headers.get('content-length')
if ( content.headers && content.body && length ) {
request.response.body = content.body
request.response.headers.set('Content-Length', length)
} else {
this.logger.debug(`Tried to serve file that does not exist: ${this.path}`)
request.response.status = HTTPStatus.NOT_FOUND
}
return request
}
}

View File

@ -9,6 +9,7 @@ import {HTTPStatus} from '../../const/http.ts'
import HTTPErrorResponseFactory from './HTTPErrorResponseFactory.ts' import HTTPErrorResponseFactory from './HTTPErrorResponseFactory.ts'
import HTTPError from '../../error/HTTPError.ts' import HTTPError from '../../error/HTTPError.ts'
import ViewResponseFactory from './ViewResponseFactory.ts' import ViewResponseFactory from './ViewResponseFactory.ts'
import FileResponseFactory from './FileResponseFactory.ts'
/** /**
* Get a new JSON response factory that writes the given object as JSON. * Get a new JSON response factory that writes the given object as JSON.
@ -76,3 +77,12 @@ export function http(status: HTTPStatus, message?: string): HTTPErrorResponseFac
export function view(view: string, data?: any): ViewResponseFactory { export function view(view: string, data?: any): ViewResponseFactory {
return make(ViewResponseFactory, view, data) return make(ViewResponseFactory, view, data)
} }
/**
* Get a new file response factory for the given file path.
* @param {string} path
* @return FileResponseFactory
*/
export function file(path: string): FileResponseFactory {
return make(FileResponseFactory, path)
}

View File

@ -0,0 +1,75 @@
import Middleware from '../Middleware.ts'
import {Request} from '../Request.ts'
import {Injectable} from '../../../../di/src/decorator/Injection.ts'
import {file, http} from '../response/helpers.ts'
import {HTTPStatus} from '../../const/http.ts'
import {Logging} from '../../service/logging/Logging.ts'
/**
* Daton-provided middleware that serves files from a static directory.
* @extends Middleware
*/
@Injectable()
export default class StaticServer extends Middleware {
constructor(
protected readonly logger: Logging,
) { super() }
/**
* Handle an incoming request. Get the path to the file from the route params, and
* resolve it using the asset_dir argument. If the file exists, serve it.
* @param {Request} request
* @param {string} asset_dir
*/
public async handleRequest(request: Request, asset_dir?: string) {
if ( !asset_dir ) {
throw new Error(`This static server is mis-configured. You must provide, as an argument in the route definition, the relative path to the base directory of the static files to be served.`)
}
const params = request.route.params
const rel_file = params.$1
if ( !rel_file || !rel_file.trim() || rel_file.trim().startsWith('/') || rel_file.trim().startsWith('.') ) {
this.logger.info(`Blocked attempt to access invalid static file path: "${rel_file}"`)
return http(HTTPStatus.NOT_FOUND)
}
const abs_file = this.resolve(asset_dir, rel_file)
if ( !(await this.fileExists(abs_file)) ) {
this.logger.debug(`File does not exist: ${abs_file}`)
return http(HTTPStatus.NOT_FOUND)
}
return file(abs_file)
}
/**
* Given the asset base dir and a relative path, resolve the fully-qualified
* path to the file.
* @param {string} asset_dir
* @param {string} rel_file
* @return string
*/
protected resolve(asset_dir: string, rel_file: string): string {
return this.app.path(asset_dir, rel_file)
}
/**
* Resolves true if the given path exists, and is a file.
* @param {string} path
* @return Promise<boolean>
*/
protected async fileExists(path: string): Promise<boolean> {
try {
const stat = await Deno.lstat(path)
return stat && stat.isFile
} catch (e) {
if ( e && e instanceof Deno.errors.NotFound ) {
return false
} else {
throw e
}
}
}
}

View File

@ -1,4 +1,5 @@
import {logger} from '../../service/logging/global.ts' import {logger} from '../../service/logging/global.ts'
import {RouteHandlerDefinition} from '../../unit/Routing.ts'
/** /**
* Type representing valid HTTP verbs. * Type representing valid HTTP verbs.
@ -8,7 +9,7 @@ export type RouteVerb = 'get' | 'post' | 'patch' | 'delete' | 'head' | 'put' | '
/** /**
* Type representing a route verb group from a router definition. * Type representing a route verb group from a router definition.
*/ */
export type RouteVerbGroup = { [key: string]: string | string[] } export type RouteVerbGroup = { [key: string]: RouteHandlerDefinition | RouteHandlerDefinition[] }
/** /**
* Type representing a router definition. * Type representing a router definition.
@ -51,10 +52,24 @@ export function isRouteVerbGroup(something: any): something is RouteVerbGroup {
return false return false
} }
if ( if (
!(typeof something[key] === 'string') !(Array.isArray(something[key]) && something[key].every((x: any) => {
&& !(Array.isArray(something[key]) && something[key].every((x: any) => typeof x === 'string')) return (
typeof x === 'string'
|| (
typeof x === 'object'
&& typeof x.handler === 'string'
)
)
}))
&& !(
typeof something[key] === 'string'
|| (
typeof something[key] === 'object'
&& typeof something[key].handler === 'string'
)
)
) { ) {
logger.info(`Route verb group for key ${key} is not a string or array of strings.`) logger.info(`Route verb group for key ${key} is not valid. Must be valid RouteHandlerDefinition, or array of them.`)
return false return false
} }
} }

View File

@ -0,0 +1,14 @@
import {FakeCanonical} from './FakeCanonical.ts'
import Middleware from '../http/Middleware.ts'
import {Unit} from '../lifecycle/decorators.ts'
import StaticServer from '../http/system_middleware/StaticServer.ts'
@Unit()
export default class DatonMiddleware extends FakeCanonical<Middleware> {
protected canonical_item = 'daton'
public async up() {
await super.up()
this._items['static'] = this.make(StaticServer)
}
}

View File

@ -0,0 +1,10 @@
import {Canonical} from './Canonical.ts'
import {Canon} from './Canon.ts'
export class FakeCanonical<T> extends Canonical<T> {
public async up() {
this.make(Canon).register_canonical(this)
}
}

View File

@ -19,19 +19,28 @@ import {RegExRoute} from "../http/routing/RegExRoute.ts";
export type RouteHandlerReturnValue = SyncRouteHandlerReturnValue | AsyncRouteHandlerReturnValue export type RouteHandlerReturnValue = SyncRouteHandlerReturnValue | AsyncRouteHandlerReturnValue
export type SyncRouteHandlerReturnValue = Request | ResponseFactory | void | undefined export type SyncRouteHandlerReturnValue = Request | ResponseFactory | void | undefined
export type AsyncRouteHandlerReturnValue = Promise<SyncRouteHandlerReturnValue> export type AsyncRouteHandlerReturnValue = Promise<SyncRouteHandlerReturnValue>
export type RouteHandlerFunction = (request: Request) => RouteHandlerReturnValue export type RouteHandlerFunction = ((request: Request, arg?: any) => RouteHandlerReturnValue) | ((request: Request) => RouteHandlerReturnValue)
export interface RouteHandlerWithArgument {
handler: string,
arg: any,
}
export type RouteHandlerDefinition = string | RouteHandlerWithArgument
/** /**
* A class that can handle requests. * A class that can handle requests.
*/ */
export interface RouteHandlerClass { export interface RouteHandlerClass {
handleRequest(request: Request): RouteHandlerReturnValue handleRequest(request: Request): RouteHandlerReturnValue
handleRequest(request: Request, arg?: any): RouteHandlerReturnValue
} }
/** /**
* Base type defining a single route handler. * Base type defining a single route handler.
*/ */
export type RouteHandler = RouteHandlerFunction | RouteHandlerClass export interface RouteHandler {
handler: RouteHandlerFunction | RouteHandlerClass,
arg: any,
}
/** /**
* Base type for a collection of route handlers. * Base type for a collection of route handlers.
@ -60,8 +69,12 @@ export interface RouteDefinition {
*/ */
export function isRouteHandler(what: any): what is RouteHandler { export function isRouteHandler(what: any): what is RouteHandler {
return ( return (
typeof what === 'function' typeof what === 'object'
|| isRouteHandlerClass(what) && what.handler
&& (
typeof what.handler === 'function'
|| isRouteHandlerClass(what.handler)
)
) )
} }
@ -72,7 +85,7 @@ export function isRouteHandler(what: any): what is RouteHandler {
*/ */
export function isRouteHandlerClass(what: any): what is RouteHandlerClass { export function isRouteHandlerClass(what: any): what is RouteHandlerClass {
return ( return (
what && typeof what.handleRequest === 'function' && what.handleRequest.length === 1 what && typeof what.handleRequest === 'function' && what.handleRequest.length >= 1
) )
} }
@ -139,13 +152,15 @@ export default class Routing extends LifecycleUnit {
* @param {Array<string>} group * @param {Array<string>} group
* @return RouteHandlers * @return RouteHandlers
*/ */
public build_handler(group: string[]): RouteHandlers { public build_handler(group: RouteHandlerDefinition[]): RouteHandlers {
const handlers: RouteHandlers = [] const handlers: RouteHandlers = []
for ( const item of group ) { for ( const item of group ) {
const ref = Canonical.resolve(item) const handler_ref = typeof item === 'object' ? item.handler : item
const arg: any = typeof item === 'object' ? item.arg : undefined
const ref = Canonical.resolve(handler_ref)
if ( !ref.resource ) { if ( !ref.resource ) {
this.logger.error(`Invalid canonical reference for route: ${item}. Reference must include resource (e.g. controller::)!`) this.logger.error(`Invalid canonical reference for route: ${handler_ref}. Reference must include resource (e.g. controller::)!`)
continue continue
} }
@ -153,10 +168,15 @@ export default class Routing extends LifecycleUnit {
const resolved = resource.get(ref.item) const resolved = resource.get(ref.item)
if ( !ref.particular ) { if ( !ref.particular ) {
if ( isRouteHandler(resolved) ) { const handler = {
handlers.push(resolved) handler: resolved,
arg,
}
if ( isRouteHandler(handler) ) {
handlers.push(handler)
} else { } else {
throw new TypeError(`Invalid canonical reference for route: ${item}. Reference is not a valid route handler.`) throw new TypeError(`Invalid canonical reference for route: ${handler_ref}. Reference is not a valid route handler.`)
} }
} else { } else {
if ( isBindable(resolved) ) { if ( isBindable(resolved) ) {
@ -165,16 +185,21 @@ export default class Routing extends LifecycleUnit {
handler = resolved.get_bound_method(ref.particular) handler = resolved.get_bound_method(ref.particular)
} catch (e) { } catch (e) {
this.logger.error(e) this.logger.error(e)
throw new Error(`Invalid canonical reference for route: ${item}. Reference particular could not be bound.`) throw new Error(`Invalid canonical reference for route: ${handler_ref}. Reference particular could not be bound.`)
} }
if ( isRouteHandler(handler) ) { const handler_obj = {
handlers.push(handler) handler,
arg
}
if ( isRouteHandler(handler_obj) ) {
handlers.push(handler_obj)
} else { } else {
throw new TypeError(`Invalid canonical reference for route: ${item}. Reference is not a valid route handler.`) throw new TypeError(`Invalid canonical reference for route: ${handler_ref}. Reference is not a valid route handler.`)
} }
} else { } else {
throw new TypeError(`Invalid canonical reference for route: ${item}. Reference specifies particular, but resolved resource is not bindable.`) throw new TypeError(`Invalid canonical reference for route: ${handler_ref}. Reference specifies particular, but resolved resource is not bindable.`)
} }
} }
} }

View File

@ -6,7 +6,6 @@ import {Model} from '../Model.ts'
*/ */
export function Relation(): MethodDecorator { export function Relation(): MethodDecorator {
return (target: any, propertyKey, descriptor) => { return (target: any, propertyKey, descriptor) => {
console.log('relation decorator', target, propertyKey, descriptor)
// @ts-ignore // @ts-ignore
const original = descriptor.value const original = descriptor.value