support route handler arguments; add daton::static file server
This commit is contained in:
parent
27ee1a552b
commit
601649e699
10
TODO.txt
10
TODO.txt
@ -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
2
app/assets/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
@ -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'
|
||||
|
@ -14,5 +14,5 @@ export default {
|
||||
session: {
|
||||
driver: 'database', // memory | database
|
||||
model: 'http:Session', // required for database
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
2
lib/src/external/db.ts
vendored
2
lib/src/external/db.ts
vendored
@ -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'
|
||||
|
1
lib/src/external/std.ts
vendored
1
lib/src/external/std.ts
vendored
@ -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'
|
||||
|
@ -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'
|
||||
|
||||
/**
|
||||
|
@ -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 ) {
|
||||
|
45
lib/src/http/response/FileResponseFactory.ts
Normal file
45
lib/src/http/response/FileResponseFactory.ts
Normal 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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
75
lib/src/http/system_middleware/StaticServer.ts
Normal file
75
lib/src/http/system_middleware/StaticServer.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
14
lib/src/unit/DatonMiddleware.ts
Normal file
14
lib/src/unit/DatonMiddleware.ts
Normal 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)
|
||||
}
|
||||
}
|
10
lib/src/unit/FakeCanonical.ts
Normal file
10
lib/src/unit/FakeCanonical.ts
Normal 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)
|
||||
}
|
||||
|
||||
}
|
@ -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.`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user