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
middleware
TLS support
internationalization
uploads & universal path
CLI - view routes, template generation, start server, directives, output, args
request level error handler
develop/prod/debug modes
nicer error and home pages
favicon
utility - root, path, is_windows, is_linux, is_mac
utility - is_windows, is_linux, is_mac
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
less/scss
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 ServicesUnit } from '../../lib/src/unit/Services.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: {
driver: 'database', // memory | database
model: 'http:Session', // required for database
}
},
}

View File

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

View File

@ -10,6 +10,7 @@ import {
RoutingUnit,
ServicesUnit,
ViewEngineUnit,
DatonMiddlewareUnit,
} from './bundle/daton_units.ts'
export default [
@ -19,6 +20,7 @@ export default [
ModelsUnit,
ViewEngineUnit,
HttpKernelUnit,
DatonMiddlewareUnit,
MiddlewareUnit,
ControllerUnit,
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
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 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 * 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'

View File

@ -1,6 +1,7 @@
import {HTTPResponse} from './type/HTTPResponse.ts'
import {HTTPRequest} from './type/HTTPRequest.ts'
import {ServerRequest} from '../external/http.ts'
import {file_server} from '../external/std.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 ) {
try {
let result
if ( isRouteHandlerClass(handler) ) {
result = await handler.handleRequest(current_request)
if ( isRouteHandlerClass(handler.handler) ) {
result = await handler.handler.handleRequest(current_request, handler.arg)
} else {
result = await handler(current_request)
result = await handler.handler(current_request, handler.arg)
}
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 HTTPError from '../../error/HTTPError.ts'
import ViewResponseFactory from './ViewResponseFactory.ts'
import FileResponseFactory from './FileResponseFactory.ts'
/**
* 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 {
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 {RouteHandlerDefinition} from '../../unit/Routing.ts'
/**
* 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.
*/
export type RouteVerbGroup = { [key: string]: string | string[] }
export type RouteVerbGroup = { [key: string]: RouteHandlerDefinition | RouteHandlerDefinition[] }
/**
* Type representing a router definition.
@ -51,10 +52,24 @@ export function isRouteVerbGroup(something: any): something is RouteVerbGroup {
return false
}
if (
!(typeof something[key] === 'string')
&& !(Array.isArray(something[key]) && something[key].every((x: any) => typeof x === 'string'))
!(Array.isArray(something[key]) && something[key].every((x: any) => {
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
}
}

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 SyncRouteHandlerReturnValue = Request | ResponseFactory | void | undefined
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.
*/
export interface RouteHandlerClass {
handleRequest(request: Request): RouteHandlerReturnValue
handleRequest(request: Request, arg?: any): RouteHandlerReturnValue
}
/**
* 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.
@ -60,8 +69,12 @@ export interface RouteDefinition {
*/
export function isRouteHandler(what: any): what is RouteHandler {
return (
typeof what === 'function'
|| isRouteHandlerClass(what)
typeof what === 'object'
&& 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 {
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
* @return RouteHandlers
*/
public build_handler(group: string[]): RouteHandlers {
public build_handler(group: RouteHandlerDefinition[]): RouteHandlers {
const handlers: RouteHandlers = []
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 ) {
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
}
@ -153,10 +168,15 @@ export default class Routing extends LifecycleUnit {
const resolved = resource.get(ref.item)
if ( !ref.particular ) {
if ( isRouteHandler(resolved) ) {
handlers.push(resolved)
const handler = {
handler: resolved,
arg,
}
if ( isRouteHandler(handler) ) {
handlers.push(handler)
} 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 {
if ( isBindable(resolved) ) {
@ -165,16 +185,21 @@ export default class Routing extends LifecycleUnit {
handler = resolved.get_bound_method(ref.particular)
} catch (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) ) {
handlers.push(handler)
const handler_obj = {
handler,
arg
}
if ( isRouteHandler(handler_obj) ) {
handlers.push(handler_obj)
} 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 {
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 {
return (target: any, propertyKey, descriptor) => {
console.log('relation decorator', target, propertyKey, descriptor)
// @ts-ignore
const original = descriptor.value