|
@@ -4,8 +4,6 @@ import {Language, Resource, Charset, MediaType, LanguageStatusMessageMap} from ' |
|
|
import https from 'https'; |
|
|
import https from 'https'; |
|
|
import Negotiator from 'negotiator'; |
|
|
import Negotiator from 'negotiator'; |
|
|
import {constants} from 'http2'; |
|
|
import {constants} from 'http2'; |
|
|
import {adjustMethod} from './extenders/method'; |
|
|
|
|
|
import {adjustUrl} from './extenders/url'; |
|
|
|
|
|
import { |
|
|
import { |
|
|
handleCreateItem, |
|
|
handleCreateItem, |
|
|
handleDeleteItem, |
|
|
handleDeleteItem, |
|
@@ -119,8 +117,13 @@ export interface Middleware<Req extends RequestContext = RequestContext> { |
|
|
(req: Req): undefined | Response | Promise<undefined | Response>; |
|
|
(req: Req): undefined | Response | Promise<undefined | Response>; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const getAllowedMiddlewares = <T extends v.BaseSchema>(resource: Resource<T>, mainResourceId: string) => { |
|
|
|
|
|
|
|
|
const getAllowedMiddlewares = <T extends v.BaseSchema>(resource?: Resource<T>, mainResourceId = '') => { |
|
|
const middlewares = [] as [string, Middleware, v.BaseSchema?][]; |
|
|
const middlewares = [] as [string, Middleware, v.BaseSchema?][]; |
|
|
|
|
|
|
|
|
|
|
|
if (typeof resource === 'undefined') { |
|
|
|
|
|
return middlewares; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
if (mainResourceId === '') { |
|
|
if (mainResourceId === '') { |
|
|
if (resource.state.canFetchCollection) { |
|
|
if (resource.state.canFetchCollection) { |
|
|
middlewares.push(['GET', handleGetCollection]); |
|
|
middlewares.push(['GET', handleGetCollection]); |
|
@@ -258,118 +261,58 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr |
|
|
req.host = serverParams.host ?? 'localhost'; |
|
|
req.host = serverParams.host ?? 'localhost'; |
|
|
req.scheme = isHttps ? 'https' : 'http'; |
|
|
req.scheme = isHttps ? 'https' : 'http'; |
|
|
req.cn = req.backend.cn; |
|
|
req.cn = req.backend.cn; |
|
|
|
|
|
req.method = req.method?.trim().toUpperCase() ?? ''; |
|
|
|
|
|
|
|
|
|
|
|
const theBasePathUrl = req.basePath ?? ''; |
|
|
|
|
|
const basePath = new URL(theBasePathUrl, 'http://localhost'); |
|
|
|
|
|
const parsedUrl = new URL(`${theBasePathUrl}/${req.url ?? ''}`, 'http://localhost'); |
|
|
|
|
|
req.rawUrl = req.url; |
|
|
|
|
|
req.url = req.url?.slice(basePath.pathname.length) ?? ''; |
|
|
|
|
|
req.query = parsedUrl.searchParams; |
|
|
|
|
|
|
|
|
adjustRequestForContentNegotiation(req, res); |
|
|
adjustRequestForContentNegotiation(req, res); |
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
adjustMethod(req); |
|
|
|
|
|
} catch (errRaw) { |
|
|
|
|
|
if (typeof errRaw !== 'undefined') { |
|
|
|
|
|
const err= errRaw as HttpMiddlewareError; |
|
|
|
|
|
const errBody = err.response.body; |
|
|
|
|
|
if (typeof errBody !== 'undefined') { |
|
|
|
|
|
res.writeHead(err.response.statusCode, { |
|
|
|
|
|
...(err.response.headers ?? {}), |
|
|
|
|
|
'Content-Language': req.backend.cn.language.name, |
|
|
|
|
|
'Content-Type': [ |
|
|
|
|
|
req.backend.cn.mediaType.name, |
|
|
|
|
|
`charset="${req.backend.cn.charset.name}"` |
|
|
|
|
|
].join('; '), |
|
|
|
|
|
}); |
|
|
|
|
|
res.statusMessage = err.response.statusMessage ?? ''; |
|
|
|
|
|
const errBodySerialized = req.backend.cn.mediaType.serialize(errBody); |
|
|
|
|
|
const errBodyEncoded = typeof errBodySerialized !== 'undefined' ? req.backend.cn.charset.encode(errBodySerialized) : undefined; |
|
|
|
|
|
res.end(errBodyEncoded); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
res.writeHead(err.response.statusCode, { |
|
|
|
|
|
...(err.response.headers ?? {}), |
|
|
|
|
|
'Content-Language': req.backend.cn.language.name, |
|
|
|
|
|
}); |
|
|
|
|
|
res.statusMessage = err.response.statusMessage ?? ''; |
|
|
|
|
|
res.end(); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
let middlewareState; |
|
|
|
|
|
if (req.url === '/' || req.url === '') { |
|
|
|
|
|
middlewareState = await handleGetRoot(req); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
adjustUrl(req); |
|
|
|
|
|
} catch (errRaw) { |
|
|
|
|
|
if (typeof errRaw !== 'undefined') { |
|
|
|
|
|
const err= errRaw as HttpMiddlewareError; |
|
|
|
|
|
const errBody = err.response.body; |
|
|
|
|
|
if (typeof errBody !== 'undefined') { |
|
|
|
|
|
res.writeHead(err.response.statusCode, { |
|
|
|
|
|
...(err.response.headers ?? {}), |
|
|
|
|
|
'Content-Language': req.backend.cn.language.name, |
|
|
|
|
|
'Content-Type': [ |
|
|
|
|
|
req.backend.cn.mediaType.name, |
|
|
|
|
|
`charset="${req.backend.cn.charset.name}"` |
|
|
|
|
|
].join('; '), |
|
|
|
|
|
}); |
|
|
|
|
|
res.statusMessage = err.response.statusMessage ?? ''; |
|
|
|
|
|
const errBodySerialized = req.backend.cn.mediaType.serialize(errBody); |
|
|
|
|
|
const errBodyEncoded = typeof errBodySerialized !== 'undefined' ? req.backend.cn.charset.encode(errBodySerialized) : undefined; |
|
|
|
|
|
res.end(errBodyEncoded); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
res.writeHead(err.response.statusCode, { |
|
|
|
|
|
...(err.response.headers ?? {}), |
|
|
|
|
|
'Content-Language': req.backend.cn.language.name, |
|
|
|
|
|
}); |
|
|
|
|
|
res.statusMessage = err.response.statusMessage ?? ''; |
|
|
|
|
|
|
|
|
if (typeof middlewareState === 'undefined') { |
|
|
|
|
|
const [, resourceRouteName, resourceId = ''] = req.url?.split('/') ?? []; |
|
|
|
|
|
const resource = Array.from(req.backend.app.resources).find((r) => r.state!.routeName === resourceRouteName); |
|
|
|
|
|
if (typeof resource === 'undefined') { |
|
|
|
|
|
res.statusCode = constants.HTTP_STATUS_NOT_FOUND; |
|
|
|
|
|
res.statusMessage = req.backend.cn.language.statusMessages.urlNotFound(); |
|
|
res.end(); |
|
|
res.end(); |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (req.url === '/') { |
|
|
|
|
|
const middlewareState = await handleGetRoot(req); |
|
|
|
|
|
if (typeof middlewareState !== 'undefined') { |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
req.resource = resource as BackendResource; |
|
|
|
|
|
req.resource.dataSource = req.backend.dataSource(req.resource) as DataSource; |
|
|
|
|
|
req.resourceId = resourceId; |
|
|
|
|
|
|
|
|
res.writeHead(constants.HTTP_STATUS_METHOD_NOT_ALLOWED, { |
|
|
|
|
|
Allow: 'HEAD, GET' |
|
|
|
|
|
}); |
|
|
|
|
|
res.statusMessage = req.backend.cn.language.statusMessages.methodNotAllowed(); |
|
|
|
|
|
res.end(); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const [, resourceRouteName, resourceId = ''] = req.url?.split('/') ?? []; |
|
|
|
|
|
const resource = Array.from(req.backend.app.resources).find((r) => r.state!.routeName === resourceRouteName); |
|
|
|
|
|
if (typeof resource === 'undefined') { |
|
|
|
|
|
res.statusCode = constants.HTTP_STATUS_NOT_FOUND; |
|
|
|
|
|
res.statusMessage = req.backend.cn.language.statusMessages.urlNotFound(); |
|
|
|
|
|
res.end(); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
req.resource = resource as BackendResource; |
|
|
|
|
|
req.resource.dataSource = req.backend.dataSource(req.resource) as DataSource; |
|
|
|
|
|
req.resourceId = resourceId; |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
await req.resource.dataSource.initialize(); |
|
|
|
|
|
} catch (cause) { |
|
|
|
|
|
throw new HttpMiddlewareError( |
|
|
|
|
|
'unableToInitializeResourceDataSource', |
|
|
|
|
|
{ |
|
|
|
|
|
cause, |
|
|
|
|
|
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, |
|
|
|
|
|
} |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
await req.resource.dataSource.initialize(); |
|
|
|
|
|
} catch (cause) { |
|
|
|
|
|
throw new HttpMiddlewareError( |
|
|
|
|
|
'unableToInitializeResourceDataSource', |
|
|
|
|
|
{ |
|
|
|
|
|
cause, |
|
|
|
|
|
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, |
|
|
|
|
|
} |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const middlewares = getAllowedMiddlewares(resource, resourceId); |
|
|
|
|
|
const middlewareState = await middlewares.reduce<unknown>( |
|
|
|
|
|
|
|
|
const middlewares = getAllowedMiddlewares(req.resource, req.resourceId ?? ''); |
|
|
|
|
|
middlewareState = await middlewares.reduce<unknown>( |
|
|
async (currentHandlerStatePromise, currentValue) => { |
|
|
async (currentHandlerStatePromise, currentValue) => { |
|
|
const [middlewareMethod, middleware, schema] = currentValue; |
|
|
const [middlewareMethod, middleware, schema] = currentValue; |
|
|
try { |
|
|
try { |
|
|
const currentHandlerState = await currentHandlerStatePromise; |
|
|
const currentHandlerState = await currentHandlerStatePromise; |
|
|
|
|
|
const effectiveMethod = req.method === 'HEAD' ? 'GET' : req.method; |
|
|
|
|
|
|
|
|
if (req.method !== middlewareMethod) { |
|
|
|
|
|
|
|
|
if (effectiveMethod !== middlewareMethod) { |
|
|
return currentHandlerState; |
|
|
return currentHandlerState; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
@@ -401,6 +344,12 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr |
|
|
|
|
|
|
|
|
const result = await middleware(req); |
|
|
const result = await middleware(req); |
|
|
|
|
|
|
|
|
|
|
|
if (req.method === 'HEAD' && result instanceof PlainResponse) { |
|
|
|
|
|
const { body: _, ...etcResult } = result; |
|
|
|
|
|
|
|
|
|
|
|
return Promise.resolve(new PlainResponse(etcResult)); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
return Promise.resolve(result); |
|
|
return Promise.resolve(result); |
|
|
} catch (errRaw) { |
|
|
} catch (errRaw) { |
|
|
// todo use error message key for each method |
|
|
// todo use error message key for each method |
|
@@ -418,8 +367,8 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr |
|
|
return errRaw; |
|
|
return errRaw; |
|
|
} |
|
|
} |
|
|
}, |
|
|
}, |
|
|
Promise.resolve<ReturnType<Middleware>>(undefined) |
|
|
|
|
|
) as Awaited<ReturnType<Middleware>>; |
|
|
|
|
|
|
|
|
Promise.resolve<ReturnType<Middleware> | HttpMiddlewareError>(middlewareState) |
|
|
|
|
|
) as Awaited<ReturnType<Middleware> | HttpMiddlewareError>; |
|
|
|
|
|
|
|
|
if (typeof middlewareState !== 'undefined') { |
|
|
if (typeof middlewareState !== 'undefined') { |
|
|
try { |
|
|
try { |
|
@@ -474,8 +423,9 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr |
|
|
].join('; '); |
|
|
].join('; '); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const statusMessageFn = middlewareState.statusMessage ? req.cn.language.statusMessages[middlewareState.statusMessage] : undefined; |
|
|
|
|
|
res.statusMessage = statusMessageFn?.(req.resource) ?? ''; |
|
|
res.writeHead(middlewareState.statusCode, headers); |
|
|
res.writeHead(middlewareState.statusCode, headers); |
|
|
res.statusMessage = middlewareState.statusMessage ?? ''; |
|
|
|
|
|
if (typeof encoded !== 'undefined') { |
|
|
if (typeof encoded !== 'undefined') { |
|
|
res.end(encoded); |
|
|
res.end(encoded); |
|
|
return; |
|
|
return; |
|
@@ -508,8 +458,9 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr |
|
|
`charset=${req.backend.cn.charset.name}` |
|
|
`charset=${req.backend.cn.charset.name}` |
|
|
].join('; '); |
|
|
].join('; '); |
|
|
|
|
|
|
|
|
|
|
|
const statusMessageFn = finalErr.response.statusMessage ? req.backend.cn.language.statusMessages[finalErr.response.statusMessage] : undefined; |
|
|
|
|
|
res.statusMessage = statusMessageFn?.(req.resource) ?? ''; |
|
|
res.writeHead(finalErr.response.statusCode, headers); |
|
|
res.writeHead(finalErr.response.statusCode, headers); |
|
|
res.statusMessage = finalErr.response.statusMessage ?? ''; |
|
|
|
|
|
if (typeof encoded !== 'undefined') { |
|
|
if (typeof encoded !== 'undefined') { |
|
|
res.end(encoded); |
|
|
res.end(encoded); |
|
|
return; |
|
|
return; |
|
@@ -520,16 +471,17 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (middlewares.length > 0) { |
|
|
if (middlewares.length > 0) { |
|
|
|
|
|
res.statusMessage = req.backend.cn.language.statusMessages.methodNotAllowed(); |
|
|
res.writeHead(constants.HTTP_STATUS_METHOD_NOT_ALLOWED, { |
|
|
res.writeHead(constants.HTTP_STATUS_METHOD_NOT_ALLOWED, { |
|
|
Allow: middlewares.map((m) => m[0]).join(', ') |
|
|
Allow: middlewares.map((m) => m[0]).join(', ') |
|
|
}); |
|
|
}); |
|
|
res.statusMessage = req.backend.cn.language.statusMessages.methodNotAllowed(); |
|
|
|
|
|
res.end(); |
|
|
res.end(); |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
res.statusCode = constants.HTTP_STATUS_NOT_FOUND; |
|
|
|
|
|
|
|
|
// TODO error handler in line with authentication |
|
|
res.statusMessage = req.backend.cn.language.statusMessages.urlNotFound(); |
|
|
res.statusMessage = req.backend.cn.language.statusMessages.urlNotFound(); |
|
|
|
|
|
res.writeHead(constants.HTTP_STATUS_NOT_FOUND); |
|
|
res.end(); |
|
|
res.end(); |
|
|
return; |
|
|
return; |
|
|
}); |
|
|
}); |
|
|